From 60ff3c4aed5a888423a4e6d13d35f94925801260 Mon Sep 17 00:00:00 2001 From: tnfAngel <57068341+tnfAngel@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:35:08 +0100 Subject: [PATCH] avatar impl --- src/api/api.ts | 14 ++ src/components/modals/EditAccountModal.tsx | 165 +++++++++++++++++++++ src/components/screens/ActivityScreen.tsx | 9 +- src/components/screens/ClassroomScreen.tsx | 54 +++++-- src/components/screens/ProfileScreen.tsx | 12 +- 5 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 src/components/modals/EditAccountModal.tsx diff --git a/src/api/api.ts b/src/api/api.ts index 246288d..a02cb9e 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -9,6 +9,20 @@ const getAuthHeaders = (): Record => { }; export const api = { + user: { + avatar: async (): Promise<{ url: string }> => { + const res = await fetch(`${API_URL}/user/avatar`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders() + } + }); + + if (!res.ok) throw new Error('Failed to change avatar'); + return res.json(); + } + }, classroom: { getAll: async (): Promise => { const res = await fetch(`${API_URL}/classrooms`, { diff --git a/src/components/modals/EditAccountModal.tsx b/src/components/modals/EditAccountModal.tsx new file mode 100644 index 0000000..91ef10f --- /dev/null +++ b/src/components/modals/EditAccountModal.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { api } from '@/api/api'; +import { CDN_URL } from '@/constants/constants'; +import { authAtom, loadUser } from '@/store/auth'; +import { + Avatar, + Button, + FormControl, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + VStack, + useToast +} from '@chakra-ui/react'; +import { useAtom } from 'jotai'; +import { useCallback, useEffect, useState } from 'react'; +import { useDropzone } from 'react-dropzone'; + +interface EditAccountModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function EditAccountModal({ isOpen, onClose }: Readonly) { + const [isLoading, setIsLoading] = useState(false); + const [avatarFile, setAvatarFile] = useState(null); + const [avatarPreview, setAvatarPreview] = useState(null); + const toast = useToast(); + const [auth, setAuth] = useAtom(authAtom); + + const onDrop = useCallback((acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + if (file) { + setAvatarFile(file); + const reader = new FileReader(); + reader.onloadend = () => { + setAvatarPreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }, []); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + accept: { + 'image/*': ['.png', '.jpg', '.jpeg', '.gif'] + }, + maxFiles: 1, + maxSize: 10 * 1024 * 1024 + }); + + useEffect(() => { + if (!isOpen) { + setAvatarFile(null); + setAvatarPreview(null); + } + }, [isOpen]); + + const handleSubmit = async () => { + if (!avatarFile) { + toast({ + title: 'Error', + description: 'Por favor selecciona una imagen', + status: 'error', + position: 'top-right', + duration: 3000, + isClosable: true + }); + return; + } + + setIsLoading(true); + try { + const res = await api.user.avatar(); + if (!res) throw new Error('Failed to get signed URL'); + + const uploadRes = await fetch(res.url, { + method: 'PUT', + body: avatarFile, + headers: { + 'Content-Type': avatarFile.type + } + }); + + if (!uploadRes.ok) throw new Error('Failed to upload file to S3'); + + if (auth.token) { + const updatedUser = await loadUser(auth.token); + setAuth((prev) => ({ ...prev, user: updatedUser })); + } + + toast({ + title: 'Éxito', + description: 'Tu avatar ha sido actualizado correctamente', + status: 'success', + position: 'top-right', + duration: 3000, + isClosable: true + }); + onClose(); + } catch { + toast({ + title: 'Error', + description: 'Ha ocurrido un error al actualizar tu avatar', + status: 'error', + position: 'top-right', + duration: 3000, + isClosable: true + }); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + Editar Cuenta + + + + + Avatar + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/screens/ActivityScreen.tsx b/src/components/screens/ActivityScreen.tsx index 120d887..b0bf27e 100644 --- a/src/components/screens/ActivityScreen.tsx +++ b/src/components/screens/ActivityScreen.tsx @@ -29,6 +29,7 @@ import { import { format } from 'date-fns'; import { es } from 'date-fns/locale'; import { useAtom } from 'jotai'; +import NextLink from 'next/link'; import { useCallback, useEffect, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { FiCalendar, FiDownload, FiExternalLink, FiFileText, FiSend, FiUpload, FiX } from 'react-icons/fi'; @@ -194,7 +195,13 @@ export default function ActivityScreen({ > {activityTypeInfo[activity.type].label} - {classroom.name} + + {classroom.name} + ) { } catch { toast({ title: 'Error', - description: 'No se pudo cargar la lista de estudiantes', + description: 'No se pudo cargar la lista de miembros', status: 'error', position: 'top-right', duration: 3000, @@ -112,6 +113,8 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) { if (!classroom) return null; + const students = classMembers.filter((m) => m.id !== classroom.owner); + return ( @@ -162,15 +165,28 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) { {classroom.owner === auth.user?.id && ( - Código: {classroom.code} + + Código: {classroom.code} + )} - - {professor?.username} + + + {professor?.username} + + Profesor @@ -294,7 +310,15 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) { borderColor='brand.dark.800' mb='20px' > - + {professor.username} @@ -321,9 +345,8 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) { }} gap={4} > - {classMembers - .filter((m) => m.id !== classroom.owner) - .map((member) => ( + {students.length > 0 ? ( + students.map((member) => ( ) { border='1px solid' borderColor='brand.dark.800' > - + {member.username} @@ -342,7 +373,10 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) { - ))} + )) + ) : ( + Sin estudiantes + )} )} diff --git a/src/components/screens/ProfileScreen.tsx b/src/components/screens/ProfileScreen.tsx index f266f19..ddd8323 100644 --- a/src/components/screens/ProfileScreen.tsx +++ b/src/components/screens/ProfileScreen.tsx @@ -22,10 +22,13 @@ import { useEffect, useState } from 'react'; import { FiPlus, FiUsers } from 'react-icons/fi'; import ClassroomCard from '../general/ClassroomCard'; import CreateClassModal from '../modals/CreateClassModal'; +import EditAccountModal from '../modals/EditAccountModal'; +import { CDN_URL } from '@/constants/constants'; export default function ProfileScreen() { const [auth] = useAtom(authAtom); const { isOpen: isCreateOpen, onOpen: onCreateOpen, onClose: onCreateClose } = useDisclosure(); + const { isOpen: isEditOpen, onOpen: onEditOpen, onClose: onEditClose } = useDisclosure(); const [classrooms, setClassrooms] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -60,7 +63,11 @@ export default function ProfileScreen() { alignItems='center' > - + {user.username} {user.email} @@ -71,7 +78,7 @@ export default function ProfileScreen() { - @@ -120,6 +127,7 @@ export default function ProfileScreen() { + ) : ( <>