44 */
55
66import React , { useState , useCallback , useRef } from 'react' ;
7- import {
8- Upload ,
9- FileText ,
10- Code ,
11- Video ,
12- Music ,
13- Clock ,
7+ import {
8+ Upload ,
9+ FileText ,
10+ Code ,
11+ Video ,
12+ Music ,
13+ Clock ,
1414 AlertCircle ,
1515 CheckCircle ,
1616 Save ,
@@ -19,6 +19,11 @@ import {
1919 File
2020} from 'lucide-react' ;
2121import { toast } from 'react-hot-toast' ;
22+ import {
23+ assignmentTextSchema ,
24+ assignmentCodeSchema ,
25+ validateFile as validateFileMeta ,
26+ } from '@/lib/schemas' ;
2227
2328interface Assignment {
2429 id : string ;
@@ -71,6 +76,9 @@ export default function AssignmentSubmission({
7176 const [ isSaving , setIsSaving ] = useState ( false ) ;
7277 const [ uploadedFiles , setUploadedFiles ] = useState < UploadedFile [ ] > ( existingSubmission ?. files || [ ] ) ;
7378 const [ dragActive , setDragActive ] = useState ( false ) ;
79+ // Inline form errors. Shape is keyed by submissionType + field when useful.
80+ const [ formError , setFormError ] = useState < string | null > ( null ) ;
81+ const [ fileErrors , setFileErrors ] = useState < Record < string , string > > ( { } ) ;
7482 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
7583
7684 const isLate = new Date ( ) > new Date ( assignment . dueDate ) ;
@@ -91,25 +99,40 @@ export default function AssignmentSubmission({
9199 if ( ! files ) return ;
92100
93101 const newFiles : UploadedFile [ ] = [ ] ;
102+ const nextFileErrors : Record < string , string > = { ...fileErrors } ;
94103 const maxSize = assignment . maxFileSize ? assignment . maxFileSize * 1024 * 1024 : 100 * 1024 * 1024 ; // Default 100MB
95104 const maxFiles = assignment . maxFiles || 10 ;
96105
97106 Array . from ( files ) . forEach ( ( file ) => {
98107 if ( uploadedFiles . length + newFiles . length >= maxFiles ) {
99- toast . error ( `Maximum ${ maxFiles } files allowed` ) ;
108+ const msg = `Maximum ${ maxFiles } files allowed` ;
109+ toast . error ( msg ) ;
110+ nextFileErrors [ file . name ] = msg ;
100111 return ;
101112 }
102113
114+ // Schema-level validation (size + mime type). We additionally enforce
115+ // the caller's `maxSize` override and `allowedFileTypes` extension
116+ // list because `fileMetaSchema` doesn't see component props.
117+ const schemaResult = validateFileMeta ( file ) ;
118+ if ( ! schemaResult . valid ) {
119+ toast . error ( `${ file . name } : ${ schemaResult . error } ` ) ;
120+ nextFileErrors [ file . name ] = schemaResult . error ;
121+ return ;
122+ }
103123 if ( file . size > maxSize ) {
104- toast . error ( `File ${ file . name } exceeds maximum size limit` ) ;
124+ const msg = `File ${ file . name } exceeds maximum size limit` ;
125+ toast . error ( msg ) ;
126+ nextFileErrors [ file . name ] = msg ;
105127 return ;
106128 }
107129
108- // Check file type if restrictions exist
109130 if ( assignment . allowedFileTypes && assignment . allowedFileTypes . length > 0 ) {
110131 const fileExtension = '.' + file . name . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ;
111132 if ( ! assignment . allowedFileTypes . includes ( fileExtension ) ) {
112- toast . error ( `File type ${ fileExtension } is not allowed` ) ;
133+ const msg = `File type ${ fileExtension } is not allowed` ;
134+ toast . error ( msg ) ;
135+ nextFileErrors [ file . name ] = msg ;
113136 return ;
114137 }
115138 }
@@ -124,7 +147,14 @@ export default function AssignmentSubmission({
124147 } ) ;
125148
126149 setUploadedFiles ( prev => [ ...prev , ...newFiles ] ) ;
127- } , [ uploadedFiles , assignment . maxFileSize , assignment . maxFiles , assignment . allowedFileTypes ] ) ;
150+ setFileErrors ( nextFileErrors ) ;
151+ } , [
152+ uploadedFiles ,
153+ fileErrors ,
154+ assignment . maxFileSize ,
155+ assignment . maxFiles ,
156+ assignment . allowedFileTypes ,
157+ ] ) ;
128158
129159 const handleDrag = useCallback ( ( e : React . DragEvent ) => {
130160 e . preventDefault ( ) ;
@@ -152,26 +182,46 @@ export default function AssignmentSubmission({
152182
153183 const handleSubmit = async ( ) => {
154184 if ( ! canSubmit ) {
185+ setFormError ( 'Submission is not allowed' ) ;
155186 toast . error ( 'Submission is not allowed' ) ;
156187 return ;
157188 }
158189
159- // Validate submission based on assignment requirements
160- if ( assignment . submissionTypes . includes ( 'text' ) && ! submissionData . textContent . trim ( ) ) {
161- toast . error ( 'Text content is required' ) ;
162- return ;
190+ // Text-only / text-bearing assignments: use Zod schema. The same
191+ // schema is also enforced server-side (or will be), so client- and
192+ // server-side copies can't drift apart.
193+ if ( assignment . submissionTypes . includes ( 'text' ) ) {
194+ const result = assignmentTextSchema . safeParse ( submissionData ) ;
195+ if ( ! result . success ) {
196+ const msg = result . error . issues [ 0 ] ?. message ?? 'Text content is required' ;
197+ setFormError ( msg ) ;
198+ toast . error ( msg ) ;
199+ return ;
200+ }
163201 }
164202
165- if ( assignment . submissionTypes . includes ( 'file' ) && uploadedFiles . length === 0 ) {
166- toast . error ( 'At least one file must be uploaded' ) ;
167- return ;
203+ if ( assignment . submissionTypes . includes ( 'code' ) ) {
204+ const result = assignmentCodeSchema . safeParse ( submissionData . codeSubmission ) ;
205+ if ( ! result . success ) {
206+ const msg = result . error . issues [ 0 ] ?. message ?? 'Code submission is required' ;
207+ setFormError ( msg ) ;
208+ toast . error ( msg ) ;
209+ return ;
210+ }
168211 }
169212
170- if ( assignment . submissionTypes . includes ( 'code' ) && ! submissionData . codeSubmission . code . trim ( ) ) {
171- toast . error ( 'Code submission is required' ) ;
213+ // File count remains a domain-level check (the Zod `fileMetaSchema`
214+ // validates *each* file but doesn't know "at least one"). File-level
215+ // metadata errors already surfaced inline via `setFileErrors`
216+ // during upload; here we gate on the count.
217+ if ( assignment . submissionTypes . includes ( 'file' ) && uploadedFiles . length === 0 ) {
218+ const msg = 'At least one file must be uploaded' ;
219+ setFormError ( msg ) ;
220+ toast . error ( msg ) ;
172221 return ;
173222 }
174223
224+ setFormError ( null ) ;
175225 setIsSubmitting ( true ) ;
176226 try {
177227 const submissionPayload = {
@@ -182,7 +232,9 @@ export default function AssignmentSubmission({
182232 await onSubmit ( submissionPayload ) ;
183233 toast . success ( 'Assignment submitted successfully!' ) ;
184234 } catch ( error ) {
185- toast . error ( 'Failed to submit assignment' ) ;
235+ const msg = 'Failed to submit assignment' ;
236+ setFormError ( msg ) ;
237+ toast . error ( msg ) ;
186238 console . error ( 'Submission error:' , error ) ;
187239 } finally {
188240 setIsSubmitting ( false ) ;
@@ -397,6 +449,18 @@ export default function AssignmentSubmission({
397449 </ div >
398450 ) }
399451
452+ { /* Top-level form error banner so screen readers pick up the news. */ }
453+ { formError && (
454+ < div
455+ role = "alert"
456+ aria-live = "assertive"
457+ className = "mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-center gap-3"
458+ >
459+ < AlertCircle className = "w-5 h-5 text-red-600" />
460+ < p className = "text-sm text-red-800" > { formError } </ p >
461+ </ div >
462+ ) }
463+
400464 { /* Action Buttons */ }
401465 < div className = "flex justify-between items-center" >
402466 < button
0 commit comments