Skip to content
This repository was archived by the owner on Jun 12, 2025. It is now read-only.
Merged

Dev #17

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ const getAuthHeaders = (): Record<string, string> => {
};

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<IClassroom[]> => {
const res = await fetch(`${API_URL}/classrooms`, {
Expand Down
165 changes: 165 additions & 0 deletions src/components/modals/EditAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -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<EditAccountModalProps>) {
const [isLoading, setIsLoading] = useState(false);
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string | null>(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 (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay backdropFilter='blur(4px)' />
<ModalContent bg='brand.dark.900' border='1px solid' borderColor='brand.dark.800'>
<ModalHeader>Editar Cuenta</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={6}>
<FormControl>
<FormLabel>Avatar</FormLabel>
<VStack spacing={4}>
<Avatar
size='2xl'
src={
avatarPreview ||
(auth.user?.avatar
? `${CDN_URL}/avatars/${auth.user?.id}/${auth.user?.avatar}.png`
: '')
}
cursor='pointer'
name={auth.user?.username}
{...(getRootProps() as any)}
/>
<Input {...(getInputProps() as any)} />
<Button size='sm' variant='outline' {...getRootProps()}>
Cambiar Avatar
</Button>
</VStack>
</FormControl>
</VStack>
</ModalBody>

<ModalFooter gap={2}>
<Button variant='ghost' onClick={onClose}>
Cancelar
</Button>
<Button colorScheme='blue' onClick={handleSubmit} isLoading={isLoading} loadingText='Guardando...'>
Guardar Cambios
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
9 changes: 8 additions & 1 deletion src/components/screens/ActivityScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -194,7 +195,13 @@ export default function ActivityScreen({
>
{activityTypeInfo[activity.type].label}
</Badge>
<Link color='gray.400'>{classroom.name}</Link>
<Link
color='gray.400'
href={`/classes/${encodeURIComponent(classroomId)}`}
as={NextLink}
>
{classroom.name}
</Link>
</Flex>
<Heading
as='h1'
Expand Down
54 changes: 44 additions & 10 deletions src/components/screens/ClassroomScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Avatar,
Box,
Button,
Code,
Container,
Flex,
Grid,
Expand Down Expand Up @@ -90,7 +91,7 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
} 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,
Expand All @@ -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 (
<Box as='main' className='animate-fade-in'>
<Box bg='brand.dark.900' py={12} position='relative' overflow='hidden'>
Expand Down Expand Up @@ -162,15 +165,28 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
{classroom.owner === auth.user?.id && (
<Flex align='center' gap={2}>
<Icon as={FiCode} />
<Text>Código: {classroom.code}</Text>
<Text>
Código: <Code bg='transparent'>{classroom.code}</Code>
</Text>
</Flex>
)}
</Flex>
</Stack>

<VStack spacing={2} align='center'>
<Avatar size='xl' name={professor?.username} src={professor?.avatar ?? ''} />
<Text color='gray.300'>{professor?.username}</Text>
<Avatar
size='xl'
name={professor?.username}
src={
professor?.avatar
? `${CDN_URL}/avatars/${professor.id}/${professor.avatar}.png`
: ''
}
/>
<Text color='gray.200' fontWeight='bold'>
{professor?.username}
</Text>
<Text color='gray.400'>Profesor</Text>
</VStack>
</Grid>
</Container>
Expand Down Expand Up @@ -294,7 +310,15 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
borderColor='brand.dark.800'
mb='20px'
>
<Avatar size='md' name={professor.username} />
<Avatar
size='md'
name={professor.username}
src={
professor?.avatar
? `${CDN_URL}/avatars/${professor.id}/${professor.avatar}.png`
: ''
}
/>
<Box>
<Text fontWeight='bold'>{professor.username}</Text>
<Text fontSize='sm' color='brand.400'>
Expand All @@ -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) => (
<Flex
key={member.id}
p={4}
Expand All @@ -334,15 +357,26 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
border='1px solid'
borderColor='brand.dark.800'
>
<Avatar size='md' name={member.username} />
<Avatar
size='md'
name={member.username}
src={
member?.avatar
? `${CDN_URL}/avatars/${member.id}/${member.avatar}.png`
: ''
}
/>
<Box>
<Text fontWeight='bold'>{member.username}</Text>
<Text fontSize='sm' color='gray.400'>
Estudiante
</Text>
</Box>
</Flex>
))}
))
) : (
<Text>Sin estudiantes</Text>
)}
</Grid>
)}
</Box>
Expand Down
12 changes: 10 additions & 2 deletions src/components/screens/ProfileScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IClassroom[]>([]);
const [isLoading, setIsLoading] = useState(true);

Expand Down Expand Up @@ -60,7 +63,11 @@ export default function ProfileScreen() {
alignItems='center'
>
<Flex align='center' gap={6}>
<Avatar size='xl' name={user.username} src={user.avatar ?? ''} />
<Avatar
size='xl'
name={user.username}
src={user?.avatar ? `${CDN_URL}/avatars/${user.id}/${user.avatar}.png` : ''}
/>
<VStack align='start' spacing={1}>
<Heading size='lg'>{user.username}</Heading>
<Text color='gray.400'>{user.email}</Text>
Expand All @@ -71,7 +78,7 @@ export default function ProfileScreen() {
<Button leftIcon={<FiPlus />} colorScheme='blue' onClick={onCreateOpen}>
Crear Clase
</Button>
<Button leftIcon={<FiUsers />} variant='outline'>
<Button leftIcon={<FiUsers />} variant='outline' onClick={onEditOpen}>
Editar cuenta
</Button>
</Flex>
Expand Down Expand Up @@ -120,6 +127,7 @@ export default function ProfileScreen() {
</Container>

<CreateClassModal isOpen={isCreateOpen} onClose={onCreateClose} onClassroomCreated={setClassrooms} />
<EditAccountModal isOpen={isEditOpen} onClose={onEditClose} />
</Box>
) : (
<></>
Expand Down