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

Commit f68c8bc

Browse files
authored
Merge pull request #20 from EVOGD-Project:dev
Dev
2 parents a87b760 + b440e8f commit f68c8bc

3 files changed

Lines changed: 286 additions & 16 deletions

File tree

src/api/api.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { API_URL } from '@/constants/constants';
22
import type { IActivity } from '@/types/IActivity';
33
import type { IClassroom } from '@/types/IClassroomCard';
4+
import type { ISubmission } from '@/types/ISubmission';
45
import type { IUser } from '@/types/IUser';
56

67
const getAuthHeaders = (): Record<string, string> => {
@@ -146,6 +147,65 @@ export const api = {
146147
return res.json();
147148
},
148149

150+
getSubmissionUrl: async (
151+
classroomId: string,
152+
activityId: string,
153+
filename: string,
154+
comment: string
155+
): Promise<{ url: string }> => {
156+
const res = await fetch(
157+
`${API_URL}/classrooms/${encodeURIComponent(classroomId)}/activities/${encodeURIComponent(
158+
activityId
159+
)}/submissions`,
160+
{
161+
method: 'POST',
162+
headers: {
163+
'Content-Type': 'application/json',
164+
...getAuthHeaders()
165+
},
166+
body: JSON.stringify({ filename, comment })
167+
}
168+
);
169+
170+
if (!res.ok) throw new Error('Failed to get submission URL');
171+
return res.json();
172+
},
173+
174+
getAllSubmissions: async (
175+
classroomId: string,
176+
activityId: string
177+
): Promise<(ISubmission & { user: IUser })[]> => {
178+
const res = await fetch(
179+
`${API_URL}/classrooms/${encodeURIComponent(classroomId)}/activities/${encodeURIComponent(
180+
activityId
181+
)}/submissions/all`,
182+
{
183+
headers: {
184+
...getAuthHeaders()
185+
}
186+
}
187+
);
188+
189+
if (!res.ok) throw new Error('Failed to fetch submissions');
190+
return res.json();
191+
},
192+
193+
getUserSubmission: async (classroomId: string, activityId: string): Promise<ISubmission> => {
194+
const res = await fetch(
195+
`${API_URL}/classrooms/${encodeURIComponent(classroomId)}/activities/${encodeURIComponent(
196+
activityId
197+
)}/submissions`,
198+
{
199+
headers: {
200+
...getAuthHeaders()
201+
}
202+
}
203+
);
204+
205+
if (!res.ok) throw new Error('Failed to fetch user submission');
206+
return res.json();
207+
},
208+
149209
create: async (
150210
classroomId: string,
151211
data: {

src/components/screens/ActivityScreen.tsx

Lines changed: 215 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
'use client';
22

33
import { api } from '@/api/api';
4+
import { CDN_URL } from '@/constants/constants';
45
import { authAtom } from '@/store/auth';
56
import type { IActivity } from '@/types/IActivity';
67
import type { IClassroom } from '@/types/IClassroomCard';
8+
import type { ISubmission } from '@/types/ISubmission';
9+
import type { IUser } from '@/types/IUser';
710
import {
11+
Avatar,
812
Badge,
913
Box,
1014
Button,
@@ -55,6 +59,9 @@ export default function ActivityScreen({
5559
const [activity, setActivity] = useState<IActivity | null>(null);
5660
const [classroom, setClassroom] = useState<IClassroom | null>(null);
5761
const [isLoading, setIsLoading] = useState(true);
62+
const [submissions, setSubmissions] = useState<(ISubmission & { user: IUser })[]>([]);
63+
const [userSubmission, setUserSubmission] = useState<ISubmission | null>(null);
64+
const [isLoadingSubmissions, setIsLoadingSubmissions] = useState(false);
5865
const [files, setFiles] = useState<File[]>([]);
5966
const [comment, setComment] = useState('');
6067
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -119,6 +126,29 @@ export default function ActivityScreen({
119126

120127
setActivity(activityData);
121128
setClassroom(classroomData);
129+
130+
if (classroomData.owner === auth.user?.id && activityData.type === 'assignment') {
131+
setIsLoadingSubmissions(true);
132+
try {
133+
const submissionsData = await api.activities.getAllSubmissions(classroomId, activityId);
134+
setSubmissions(submissionsData);
135+
} catch (error) {
136+
console.error('Failed to fetch submissions:', error);
137+
} finally {
138+
setIsLoadingSubmissions(false);
139+
}
140+
} else if (activityData.type === 'assignment') {
141+
setIsLoadingSubmissions(true);
142+
try {
143+
const submission = await api.activities.getUserSubmission(classroomId, activityId);
144+
setUserSubmission(submission);
145+
setHasSubmitted(true);
146+
} catch (error) {
147+
console.error('Failed to fetch user submission:', error);
148+
} finally {
149+
setIsLoadingSubmissions(false);
150+
}
151+
}
122152
} catch {
123153
toast({
124154
title: 'Error',
@@ -135,7 +165,7 @@ export default function ActivityScreen({
135165
};
136166

137167
fetchData();
138-
}, [activityId, classroomId, toast]);
168+
}, [activityId, classroomId, toast, auth.user?.id]);
139169

140170
const handleSubmit = async () => {
141171
if (files.length === 0) {
@@ -151,20 +181,45 @@ export default function ActivityScreen({
151181
}
152182

153183
setIsSubmitting(true);
184+
try {
185+
for (const file of files) {
186+
const { url } = await api.activities.getSubmissionUrl(classroomId, activityId, file.name, comment);
154187

155-
await new Promise((resolve) => setTimeout(resolve, 1000));
188+
const uploadRes = await fetch(url, {
189+
method: 'PUT',
190+
body: file,
191+
headers: {
192+
'Content-Type': file.type
193+
}
194+
});
156195

157-
toast({
158-
title: 'Éxito',
159-
description: 'Tu tarea ha sido enviada correctamente',
160-
status: 'success',
161-
position: 'top-right',
162-
duration: 3000,
163-
isClosable: true
164-
});
196+
if (!uploadRes.ok) {
197+
throw new Error(`Failed to upload file ${file.name}`);
198+
}
199+
}
165200

166-
setHasSubmitted(true);
167-
setIsSubmitting(false);
201+
toast({
202+
title: 'Éxito',
203+
description: 'Tu tarea ha sido enviada correctamente',
204+
status: 'success',
205+
position: 'top-right',
206+
duration: 3000,
207+
isClosable: true
208+
});
209+
210+
setHasSubmitted(true);
211+
} catch (error) {
212+
toast({
213+
title: 'Error',
214+
description: error instanceof Error ? error.message : 'Ha ocurrido un error al enviar la tarea',
215+
status: 'error',
216+
position: 'top-right',
217+
duration: 3000,
218+
isClosable: true
219+
});
220+
} finally {
221+
setIsSubmitting(false);
222+
}
168223
};
169224

170225
if (isLoading) {
@@ -368,6 +423,92 @@ export default function ActivityScreen({
368423
</Box>
369424
)}
370425

426+
{isProfessor && isAssignment && (
427+
<Box
428+
bg='brand.dark.900'
429+
p={6}
430+
borderRadius='xl'
431+
border='1px solid'
432+
borderColor='brand.dark.800'
433+
>
434+
<Heading size='md' mb={4}>
435+
Entregas de Estudiantes
436+
</Heading>
437+
{isLoadingSubmissions ? (
438+
<Flex justify='center' py={4}>
439+
<Spinner />
440+
</Flex>
441+
) : submissions.length > 0 ? (
442+
<VStack spacing={4} align='stretch'>
443+
{submissions.map((submission) => (
444+
<Box key={submission.id} p={4} bg='brand.dark.800' borderRadius='lg'>
445+
<Flex justify='space-between' align='center' mb={3}>
446+
<Flex align='center' gap={3}>
447+
<Avatar
448+
size='sm'
449+
name={submission.user?.username}
450+
src={
451+
submission.user?.avatar
452+
? `${CDN_URL}/avatars/${submission.user?.id}/${submission.user?.avatar}.png`
453+
: ''
454+
}
455+
/>
456+
<VStack align='start' spacing={0}>
457+
<Text fontWeight='bold'>
458+
{submission.user?.username}
459+
</Text>
460+
<Text fontSize='sm' color='gray.400'>
461+
Entregado el{' '}
462+
{format(
463+
new Date(submission.submittedAt),
464+
"d 'de' MMMM 'a las' HH:mm",
465+
{ locale: es }
466+
)}
467+
</Text>
468+
</VStack>
469+
</Flex>
470+
</Flex>
471+
<VStack align='stretch' spacing={2}>
472+
{submission.comment && (
473+
<Text>
474+
<b>Comentario</b>: {submission.comment}
475+
</Text>
476+
)}
477+
{submission.files.map((file, index) => (
478+
<Flex
479+
key={index}
480+
justify='space-between'
481+
align='center'
482+
p={2}
483+
bg='brand.dark.900'
484+
borderRadius='md'
485+
>
486+
<Text fontSize='sm'>{file.name}</Text>
487+
<Link
488+
href={`${CDN_URL}/submissions/${file.url}`}
489+
isExternal
490+
_hover={{ textDecoration: 'none' }}
491+
>
492+
<Button
493+
size='sm'
494+
variant='ghost'
495+
leftIcon={<FiDownload />}
496+
>
497+
Descargar
498+
</Button>
499+
</Link>
500+
</Flex>
501+
))}
502+
</VStack>
503+
</Box>
504+
))}
505+
</VStack>
506+
) : (
507+
<Text color='gray.400'>Aún no hay entregas para esta tarea.</Text>
508+
)}
509+
</Box>
510+
)}
511+
371512
{!isProfessor && isAssignment && (
372513
<Box
373514
bg='brand.dark.900'
@@ -379,10 +520,68 @@ export default function ActivityScreen({
379520
<Heading size='md' mb={4}>
380521
Enviar Tarea
381522
</Heading>
382-
{hasSubmitted ? (
383-
<Text color='green.400'>
384-
¡Tu tarea ha sido enviada! El profesor la revisará pronto.
385-
</Text>
523+
{isLoadingSubmissions ? (
524+
<Flex justify='center' py={4}>
525+
<Spinner />
526+
</Flex>
527+
) : hasSubmitted ? (
528+
<VStack spacing={4} align='stretch'>
529+
<Text color='green.400' mb={2}>
530+
¡Tu tarea ha sido enviada! El profesor la revisará pronto.
531+
</Text>
532+
{userSubmission && (
533+
<Box>
534+
{userSubmission.comment && (
535+
<Text fontWeight='semibold' color='gray.100'>
536+
Comentario:
537+
</Text>
538+
)}
539+
{userSubmission.comment && (
540+
<Text mb={4}>{userSubmission.comment}</Text>
541+
)}
542+
<Text fontWeight='semibold' color='gray.1s00' mb={2}>
543+
Archivos enviados:
544+
</Text>
545+
<VStack spacing={2} align='stretch'>
546+
{userSubmission.files.map((file, index) => (
547+
<Flex
548+
key={index}
549+
justify='space-between'
550+
align='center'
551+
p={2}
552+
bg='brand.dark.800'
553+
borderRadius='md'
554+
>
555+
<Text color='gray.300' fontSize='sm'>
556+
{file.name}
557+
</Text>
558+
<Link
559+
href={`${CDN_URL}/submissions/${file.url}`}
560+
isExternal
561+
_hover={{ textDecoration: 'none' }}
562+
>
563+
<Button
564+
size='sm'
565+
variant='ghost'
566+
leftIcon={<FiDownload />}
567+
>
568+
Descargar
569+
</Button>
570+
</Link>
571+
</Flex>
572+
))}
573+
</VStack>
574+
<Text fontSize='sm' color='gray.400' mt={2}>
575+
Enviado el{' '}
576+
{format(
577+
new Date(userSubmission.submittedAt),
578+
"d 'de' MMMM 'a las' HH:mm",
579+
{ locale: es }
580+
)}
581+
</Text>
582+
</Box>
583+
)}
584+
</VStack>
386585
) : (
387586
<VStack spacing={4} align='stretch'>
388587
<Box

src/types/ISubmission.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface ISubmission {
2+
id: string;
3+
activity: string;
4+
user: string;
5+
comment: string;
6+
files: Array<{
7+
name: string;
8+
url: string;
9+
}>;
10+
submittedAt: string;
11+
}

0 commit comments

Comments
 (0)