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

Commit 0287797

Browse files
authored
Merge pull request #17 from EVOGD-Project:dev
Dev
2 parents 0427d04 + 60ff3c4 commit 0287797

5 files changed

Lines changed: 241 additions & 13 deletions

File tree

src/api/api.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ const getAuthHeaders = (): Record<string, string> => {
99
};
1010

1111
export const api = {
12+
user: {
13+
avatar: async (): Promise<{ url: string }> => {
14+
const res = await fetch(`${API_URL}/user/avatar`, {
15+
method: 'POST',
16+
headers: {
17+
'Content-Type': 'application/json',
18+
...getAuthHeaders()
19+
}
20+
});
21+
22+
if (!res.ok) throw new Error('Failed to change avatar');
23+
return res.json();
24+
}
25+
},
1226
classroom: {
1327
getAll: async (): Promise<IClassroom[]> => {
1428
const res = await fetch(`${API_URL}/classrooms`, {
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
'use client';
2+
3+
import { api } from '@/api/api';
4+
import { CDN_URL } from '@/constants/constants';
5+
import { authAtom, loadUser } from '@/store/auth';
6+
import {
7+
Avatar,
8+
Button,
9+
FormControl,
10+
FormLabel,
11+
Input,
12+
Modal,
13+
ModalBody,
14+
ModalCloseButton,
15+
ModalContent,
16+
ModalFooter,
17+
ModalHeader,
18+
ModalOverlay,
19+
VStack,
20+
useToast
21+
} from '@chakra-ui/react';
22+
import { useAtom } from 'jotai';
23+
import { useCallback, useEffect, useState } from 'react';
24+
import { useDropzone } from 'react-dropzone';
25+
26+
interface EditAccountModalProps {
27+
isOpen: boolean;
28+
onClose: () => void;
29+
}
30+
31+
export default function EditAccountModal({ isOpen, onClose }: Readonly<EditAccountModalProps>) {
32+
const [isLoading, setIsLoading] = useState(false);
33+
const [avatarFile, setAvatarFile] = useState<File | null>(null);
34+
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
35+
const toast = useToast();
36+
const [auth, setAuth] = useAtom(authAtom);
37+
38+
const onDrop = useCallback((acceptedFiles: File[]) => {
39+
const file = acceptedFiles[0];
40+
if (file) {
41+
setAvatarFile(file);
42+
const reader = new FileReader();
43+
reader.onloadend = () => {
44+
setAvatarPreview(reader.result as string);
45+
};
46+
reader.readAsDataURL(file);
47+
}
48+
}, []);
49+
50+
const { getRootProps, getInputProps } = useDropzone({
51+
onDrop,
52+
accept: {
53+
'image/*': ['.png', '.jpg', '.jpeg', '.gif']
54+
},
55+
maxFiles: 1,
56+
maxSize: 10 * 1024 * 1024
57+
});
58+
59+
useEffect(() => {
60+
if (!isOpen) {
61+
setAvatarFile(null);
62+
setAvatarPreview(null);
63+
}
64+
}, [isOpen]);
65+
66+
const handleSubmit = async () => {
67+
if (!avatarFile) {
68+
toast({
69+
title: 'Error',
70+
description: 'Por favor selecciona una imagen',
71+
status: 'error',
72+
position: 'top-right',
73+
duration: 3000,
74+
isClosable: true
75+
});
76+
return;
77+
}
78+
79+
setIsLoading(true);
80+
try {
81+
const res = await api.user.avatar();
82+
if (!res) throw new Error('Failed to get signed URL');
83+
84+
const uploadRes = await fetch(res.url, {
85+
method: 'PUT',
86+
body: avatarFile,
87+
headers: {
88+
'Content-Type': avatarFile.type
89+
}
90+
});
91+
92+
if (!uploadRes.ok) throw new Error('Failed to upload file to S3');
93+
94+
if (auth.token) {
95+
const updatedUser = await loadUser(auth.token);
96+
setAuth((prev) => ({ ...prev, user: updatedUser }));
97+
}
98+
99+
toast({
100+
title: 'Éxito',
101+
description: 'Tu avatar ha sido actualizado correctamente',
102+
status: 'success',
103+
position: 'top-right',
104+
duration: 3000,
105+
isClosable: true
106+
});
107+
onClose();
108+
} catch {
109+
toast({
110+
title: 'Error',
111+
description: 'Ha ocurrido un error al actualizar tu avatar',
112+
status: 'error',
113+
position: 'top-right',
114+
duration: 3000,
115+
isClosable: true
116+
});
117+
} finally {
118+
setIsLoading(false);
119+
}
120+
};
121+
122+
return (
123+
<Modal isOpen={isOpen} onClose={onClose}>
124+
<ModalOverlay backdropFilter='blur(4px)' />
125+
<ModalContent bg='brand.dark.900' border='1px solid' borderColor='brand.dark.800'>
126+
<ModalHeader>Editar Cuenta</ModalHeader>
127+
<ModalCloseButton />
128+
<ModalBody>
129+
<VStack spacing={6}>
130+
<FormControl>
131+
<FormLabel>Avatar</FormLabel>
132+
<VStack spacing={4}>
133+
<Avatar
134+
size='2xl'
135+
src={
136+
avatarPreview ||
137+
(auth.user?.avatar
138+
? `${CDN_URL}/avatars/${auth.user?.id}/${auth.user?.avatar}.png`
139+
: '')
140+
}
141+
cursor='pointer'
142+
name={auth.user?.username}
143+
{...(getRootProps() as any)}
144+
/>
145+
<Input {...(getInputProps() as any)} />
146+
<Button size='sm' variant='outline' {...getRootProps()}>
147+
Cambiar Avatar
148+
</Button>
149+
</VStack>
150+
</FormControl>
151+
</VStack>
152+
</ModalBody>
153+
154+
<ModalFooter gap={2}>
155+
<Button variant='ghost' onClick={onClose}>
156+
Cancelar
157+
</Button>
158+
<Button colorScheme='blue' onClick={handleSubmit} isLoading={isLoading} loadingText='Guardando...'>
159+
Guardar Cambios
160+
</Button>
161+
</ModalFooter>
162+
</ModalContent>
163+
</Modal>
164+
);
165+
}

src/components/screens/ActivityScreen.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
import { format } from 'date-fns';
3030
import { es } from 'date-fns/locale';
3131
import { useAtom } from 'jotai';
32+
import NextLink from 'next/link';
3233
import { useCallback, useEffect, useState } from 'react';
3334
import { useDropzone } from 'react-dropzone';
3435
import { FiCalendar, FiDownload, FiExternalLink, FiFileText, FiSend, FiUpload, FiX } from 'react-icons/fi';
@@ -194,7 +195,13 @@ export default function ActivityScreen({
194195
>
195196
{activityTypeInfo[activity.type].label}
196197
</Badge>
197-
<Link color='gray.400'>{classroom.name}</Link>
198+
<Link
199+
color='gray.400'
200+
href={`/classes/${encodeURIComponent(classroomId)}`}
201+
as={NextLink}
202+
>
203+
{classroom.name}
204+
</Link>
198205
</Flex>
199206
<Heading
200207
as='h1'

src/components/screens/ClassroomScreen.tsx

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Avatar,
1111
Box,
1212
Button,
13+
Code,
1314
Container,
1415
Flex,
1516
Grid,
@@ -90,7 +91,7 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
9091
} catch {
9192
toast({
9293
title: 'Error',
93-
description: 'No se pudo cargar la lista de estudiantes',
94+
description: 'No se pudo cargar la lista de miembros',
9495
status: 'error',
9596
position: 'top-right',
9697
duration: 3000,
@@ -112,6 +113,8 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
112113

113114
if (!classroom) return null;
114115

116+
const students = classMembers.filter((m) => m.id !== classroom.owner);
117+
115118
return (
116119
<Box as='main' className='animate-fade-in'>
117120
<Box bg='brand.dark.900' py={12} position='relative' overflow='hidden'>
@@ -162,15 +165,28 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
162165
{classroom.owner === auth.user?.id && (
163166
<Flex align='center' gap={2}>
164167
<Icon as={FiCode} />
165-
<Text>Código: {classroom.code}</Text>
168+
<Text>
169+
Código: <Code bg='transparent'>{classroom.code}</Code>
170+
</Text>
166171
</Flex>
167172
)}
168173
</Flex>
169174
</Stack>
170175

171176
<VStack spacing={2} align='center'>
172-
<Avatar size='xl' name={professor?.username} src={professor?.avatar ?? ''} />
173-
<Text color='gray.300'>{professor?.username}</Text>
177+
<Avatar
178+
size='xl'
179+
name={professor?.username}
180+
src={
181+
professor?.avatar
182+
? `${CDN_URL}/avatars/${professor.id}/${professor.avatar}.png`
183+
: ''
184+
}
185+
/>
186+
<Text color='gray.200' fontWeight='bold'>
187+
{professor?.username}
188+
</Text>
189+
<Text color='gray.400'>Profesor</Text>
174190
</VStack>
175191
</Grid>
176192
</Container>
@@ -294,7 +310,15 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
294310
borderColor='brand.dark.800'
295311
mb='20px'
296312
>
297-
<Avatar size='md' name={professor.username} />
313+
<Avatar
314+
size='md'
315+
name={professor.username}
316+
src={
317+
professor?.avatar
318+
? `${CDN_URL}/avatars/${professor.id}/${professor.avatar}.png`
319+
: ''
320+
}
321+
/>
298322
<Box>
299323
<Text fontWeight='bold'>{professor.username}</Text>
300324
<Text fontSize='sm' color='brand.400'>
@@ -321,9 +345,8 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
321345
}}
322346
gap={4}
323347
>
324-
{classMembers
325-
.filter((m) => m.id !== classroom.owner)
326-
.map((member) => (
348+
{students.length > 0 ? (
349+
students.map((member) => (
327350
<Flex
328351
key={member.id}
329352
p={4}
@@ -334,15 +357,26 @@ export default function ClassroomScreen({ id }: Readonly<{ id: string }>) {
334357
border='1px solid'
335358
borderColor='brand.dark.800'
336359
>
337-
<Avatar size='md' name={member.username} />
360+
<Avatar
361+
size='md'
362+
name={member.username}
363+
src={
364+
member?.avatar
365+
? `${CDN_URL}/avatars/${member.id}/${member.avatar}.png`
366+
: ''
367+
}
368+
/>
338369
<Box>
339370
<Text fontWeight='bold'>{member.username}</Text>
340371
<Text fontSize='sm' color='gray.400'>
341372
Estudiante
342373
</Text>
343374
</Box>
344375
</Flex>
345-
))}
376+
))
377+
) : (
378+
<Text>Sin estudiantes</Text>
379+
)}
346380
</Grid>
347381
)}
348382
</Box>

src/components/screens/ProfileScreen.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ import { useEffect, useState } from 'react';
2222
import { FiPlus, FiUsers } from 'react-icons/fi';
2323
import ClassroomCard from '../general/ClassroomCard';
2424
import CreateClassModal from '../modals/CreateClassModal';
25+
import EditAccountModal from '../modals/EditAccountModal';
26+
import { CDN_URL } from '@/constants/constants';
2527

2628
export default function ProfileScreen() {
2729
const [auth] = useAtom(authAtom);
2830
const { isOpen: isCreateOpen, onOpen: onCreateOpen, onClose: onCreateClose } = useDisclosure();
31+
const { isOpen: isEditOpen, onOpen: onEditOpen, onClose: onEditClose } = useDisclosure();
2932
const [classrooms, setClassrooms] = useState<IClassroom[]>([]);
3033
const [isLoading, setIsLoading] = useState(true);
3134

@@ -60,7 +63,11 @@ export default function ProfileScreen() {
6063
alignItems='center'
6164
>
6265
<Flex align='center' gap={6}>
63-
<Avatar size='xl' name={user.username} src={user.avatar ?? ''} />
66+
<Avatar
67+
size='xl'
68+
name={user.username}
69+
src={user?.avatar ? `${CDN_URL}/avatars/${user.id}/${user.avatar}.png` : ''}
70+
/>
6471
<VStack align='start' spacing={1}>
6572
<Heading size='lg'>{user.username}</Heading>
6673
<Text color='gray.400'>{user.email}</Text>
@@ -71,7 +78,7 @@ export default function ProfileScreen() {
7178
<Button leftIcon={<FiPlus />} colorScheme='blue' onClick={onCreateOpen}>
7279
Crear Clase
7380
</Button>
74-
<Button leftIcon={<FiUsers />} variant='outline'>
81+
<Button leftIcon={<FiUsers />} variant='outline' onClick={onEditOpen}>
7582
Editar cuenta
7683
</Button>
7784
</Flex>
@@ -120,6 +127,7 @@ export default function ProfileScreen() {
120127
</Container>
121128

122129
<CreateClassModal isOpen={isCreateOpen} onClose={onCreateClose} onClassroomCreated={setClassrooms} />
130+
<EditAccountModal isOpen={isEditOpen} onClose={onEditClose} />
123131
</Box>
124132
) : (
125133
<></>

0 commit comments

Comments
 (0)