11"use client" ;
2- import { useState , useCallback , useRef } from 'react' ;
2+ import { useState , useCallback , useRef , useEffect } from 'react' ;
33import { useDropzone , FileWithPath } from 'react-dropzone' ;
44import { Button } from '@/components/ui/button' ;
55import { useToast } from '@/hooks/use-toast' ;
@@ -9,12 +9,16 @@ import { UploadCloud, File as FileIcon, X, Info, FolderUp, FileUp } from 'lucide
99import useSWR , { useSWRConfig } from 'swr' ;
1010import { Attempts } from '@/lib/types' ;
1111import { Skeleton } from '../ui/skeleton' ;
12+ import Editor from "@monaco-editor/react" ;
13+ import { Tabs , TabsList , TabsTrigger , TabsContent } from "@/components/ui/tabs" ;
1214
1315interface SubmissionUploadFormProps {
1416 problemId : string ;
1517 uploadLimits : {
1618 max_num : number ;
1719 max_size : number ; // in MB
20+ editor ?: boolean ;
21+ editor_files ?: string [ ] ;
1822 } ;
1923}
2024
@@ -47,29 +51,80 @@ function AttemptsCounter({ problemId, onLimitReached }: { problemId: string, onL
4751 ) ;
4852}
4953
54+ const getLanguageForFile = ( filename : string = '' ) => {
55+ const extension = filename . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ;
56+ switch ( extension ) {
57+ case 'cpp' :
58+ case 'cxx' :
59+ case 'h' :
60+ case 'hpp' :
61+ return 'cpp' ;
62+ case 'c' :
63+ return 'c' ;
64+ case 'py' :
65+ return 'python' ;
66+ case 'java' :
67+ return 'java' ;
68+ case 'js' :
69+ return 'javascript' ;
70+ case 'ts' :
71+ return 'typescript' ;
72+ case 'json' :
73+ return 'json' ;
74+ case 'xml' :
75+ return 'xml' ;
76+ case 'html' :
77+ return 'html' ;
78+ case 'css' :
79+ return 'css' ;
80+ case 'md' :
81+ return 'markdown' ;
82+ default :
83+ return 'plaintext' ;
84+ }
85+ } ;
86+
5087export default function SubmissionUploadForm ( { problemId, uploadLimits } : SubmissionUploadFormProps ) {
51- const [ files , setFiles ] = useState < File [ ] > ( [ ] ) ;
5288 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
5389 const [ isLimitReached , setIsLimitReached ] = useState ( false ) ;
5490 const { toast } = useToast ( ) ;
5591 const router = useRouter ( ) ;
5692 const { mutate } = useSWRConfig ( ) ;
93+
94+ // State for file upload mode
95+ const [ files , setFiles ] = useState < File [ ] > ( [ ] ) ;
5796 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
5897 const folderInputRef = useRef < HTMLInputElement > ( null ) ;
98+
99+ // State for editor mode
100+ const initialContents = Object . fromEntries ( ( uploadLimits . editor_files || [ ] ) . map ( file => [ file , '' ] ) ) ;
101+ const [ fileContents , setFileContents ] = useState < Record < string , string > > ( initialContents ) ;
102+ const [ activeFile , setActiveFile ] = useState < string > ( ( uploadLimits . editor_files || [ ] ) [ 0 ] || '' ) ;
103+ const [ monacoTheme , setMonacoTheme ] = useState ( 'light' ) ;
59104
60- const addFiles = useCallback ( ( newFiles : File [ ] ) => {
61- const allFiles = [ ...files , ...newFiles ] ;
105+ useEffect ( ( ) => {
106+ // Detect theme for Monaco editor on component mount
107+ if ( typeof window !== "undefined" && window . document . documentElement . classList . contains ( 'dark' ) ) {
108+ setMonacoTheme ( 'vs-dark' ) ;
109+ }
110+ } , [ ] ) ;
62111
63- if ( uploadLimits . max_num > 0 && allFiles . length > uploadLimits . max_num ) {
64- toast ( {
65- variant : 'destructive' ,
66- title : 'Too many files' ,
67- description : `You can upload a maximum of ${ uploadLimits . max_num } files.` ,
68- } ) ;
112+ const handleSubmit = async ( ) => {
113+ let filesToSubmit : File [ ] ;
114+
115+ if ( uploadLimits . editor ) {
116+ filesToSubmit = Object . entries ( fileContents )
117+ . map ( ( [ name , content ] ) => new File ( [ content ] , name , { type : 'text/plain' } ) ) ;
118+ } else {
119+ filesToSubmit = files ;
120+ }
121+
122+ if ( filesToSubmit . length === 0 ) {
123+ toast ( { variant : 'destructive' , title : 'No files selected or content is empty' } ) ;
69124 return ;
70125 }
71126
72- const totalSize = allFiles . reduce ( ( acc , file ) => acc + file . size , 0 ) ;
127+ const totalSize = filesToSubmit . reduce ( ( acc , file ) => acc + file . size , 0 ) ;
73128 if ( uploadLimits . max_size > 0 && totalSize > uploadLimits . max_size * 1024 * 1024 ) {
74129 toast ( {
75130 variant : 'destructive' ,
@@ -78,43 +133,11 @@ export default function SubmissionUploadForm({ problemId, uploadLimits }: Submis
78133 } ) ;
79134 return ;
80135 }
81-
82- setFiles ( prev => [ ...prev , ...newFiles ] ) ;
83- } , [ files , uploadLimits , toast ] ) ;
84-
85- const onDrop = useCallback ( ( acceptedFiles : FileWithPath [ ] ) => {
86- addFiles ( acceptedFiles ) ;
87- } , [ addFiles ] ) ;
88-
89- const handleManualFileSelect = ( event : React . ChangeEvent < HTMLInputElement > ) => {
90- if ( event . target . files ) {
91- addFiles ( Array . from ( event . target . files ) ) ;
92- }
93- // Reset the input value to allow selecting the same file/folder again
94- event . target . value = '' ;
95- } ;
96-
97- const { getRootProps, isDragActive } = useDropzone ( {
98- onDrop,
99- noClick : true ,
100- noKeyboard : true
101- } ) ;
102136
103- const removeFile = ( fileToRemove : File ) => {
104- setFiles ( files . filter ( file => file !== fileToRemove ) ) ;
105- } ;
106-
107- const handleSubmit = async ( ) => {
108- if ( files . length === 0 ) {
109- toast ( { variant : 'destructive' , title : 'No files selected' } ) ;
110- return ;
111- }
112137 setIsSubmitting ( true ) ;
113138 const formData = new FormData ( ) ;
114- files . forEach ( file => {
115- // Dynamically get the full path for the file.
116- // `path` comes from react-dropzone. `webkitRelativePath` comes from folder selection input. `name` is the fallback.
117- const filePath = ( file as FileWithPath ) . path || ( file as any ) . webkitRelativePath || file . name ;
139+ filesToSubmit . forEach ( file => {
140+ const filePath = uploadLimits . editor ? file . name : ( file as FileWithPath ) . path || ( file as any ) . webkitRelativePath || file . name ;
118141 formData . append ( 'files' , file , filePath ) ;
119142 } ) ;
120143
@@ -137,27 +160,80 @@ export default function SubmissionUploadForm({ problemId, uploadLimits }: Submis
137160 setIsSubmitting ( false ) ;
138161 }
139162 } ;
163+
164+ // --- File Upload Logic ---
165+ const addFiles = useCallback ( ( newFiles : File [ ] ) => {
166+ const allFiles = [ ...files , ...newFiles ] ;
167+ if ( uploadLimits . max_num > 0 && allFiles . length > uploadLimits . max_num ) {
168+ toast ( { variant : 'destructive' , title : 'Too many files' , description : `You can upload a maximum of ${ uploadLimits . max_num } files.` } ) ;
169+ return ;
170+ }
171+ setFiles ( prev => [ ...prev , ...newFiles ] ) ;
172+ } , [ files , uploadLimits . max_num , toast ] ) ;
173+
174+ const onDrop = useCallback ( ( acceptedFiles : FileWithPath [ ] ) => { addFiles ( acceptedFiles ) ; } , [ addFiles ] ) ;
140175
176+ const handleManualFileSelect = ( event : React . ChangeEvent < HTMLInputElement > ) => {
177+ if ( event . target . files ) { addFiles ( Array . from ( event . target . files ) ) ; }
178+ event . target . value = '' ;
179+ } ;
180+
181+ const { getRootProps, isDragActive } = useDropzone ( { onDrop, noClick : true , noKeyboard : true } ) ;
182+
183+ const removeFile = ( fileToRemove : File ) => { setFiles ( files . filter ( file => file !== fileToRemove ) ) ; } ;
184+
185+
186+ // --- Render Logic ---
187+ if ( uploadLimits . editor ) {
188+ return (
189+ < div className = "space-y-4" >
190+ < AttemptsCounter problemId = { problemId } onLimitReached = { setIsLimitReached } />
191+ < Tabs value = { activeFile } onValueChange = { setActiveFile } className = "w-full" >
192+ < TabsList className = "grid w-full" style = { { gridTemplateColumns : `repeat(${ uploadLimits . editor_files ?. length || 1 } , minmax(0, 1fr))` } } >
193+ { ( uploadLimits . editor_files || [ ] ) . map ( filename => (
194+ < TabsTrigger key = { filename } value = { filename } > { filename } </ TabsTrigger >
195+ ) ) }
196+ </ TabsList >
197+ < div className = "mt-4 rounded-md border overflow-hidden" >
198+ < Editor
199+ height = "40vh"
200+ path = { activeFile }
201+ language = { getLanguageForFile ( activeFile ) }
202+ value = { fileContents [ activeFile ] }
203+ onChange = { ( value ) => setFileContents ( prev => ( { ...prev , [ activeFile ] : value || '' } ) ) }
204+ theme = { monacoTheme }
205+ options = { { minimap : { enabled : false } , scrollBeyondLastLine : false , automaticLayout : true } }
206+ />
207+ </ div >
208+ </ Tabs >
209+ < Button onClick = { handleSubmit } disabled = { isSubmitting || isLimitReached } className = "w-full" >
210+ { isSubmitting ? 'Submitting...' : 'Submit' }
211+ </ Button >
212+ </ div >
213+ ) ;
214+ }
215+
216+ // Default to file upload form
141217 return (
142218 < div className = "space-y-4" >
143219 < AttemptsCounter problemId = { problemId } onLimitReached = { setIsLimitReached } />
144220 < div { ...getRootProps ( ) }
145- className = { `relative flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-md transition-colors
146- ${ isDragActive ? 'border-primary bg-primary/10' : 'border-border' }
147- ${ isLimitReached ? 'bg-muted opacity-50' : '' } ` }
221+ className = { `relative flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-md transition-colors
222+ ${ isDragActive ? 'border-primary bg-primary/10' : 'border-border' }
223+ ${ isLimitReached ? 'bg-muted opacity-50' : '' } ` }
148224 >
149225 < input type = "file" multiple ref = { fileInputRef } onChange = { handleManualFileSelect } style = { { display : 'none' } } disabled = { isLimitReached } />
150226 { /* @ts -ignore */ }
151227 < input type = "file" webkitdirectory = "true" ref = { folderInputRef } onChange = { handleManualFileSelect } style = { { display : 'none' } } disabled = { isLimitReached } />
152228
153229 < UploadCloud className = "h-10 w-10 text-muted-foreground" />
154230 { isLimitReached ? (
155- < p className = "mt-2 text-sm text-destructive" > You have reached the submission limit.</ p >
156- ) : (
157- < p className = "mt-2 text-sm text-muted-foreground text-center" >
158- { isDragActive ? 'Drop files or a folder here...' : 'Drag & drop files or a folder' }
159- </ p >
160- ) }
231+ < p className = "mt-2 text-sm text-destructive" > You have reached the submission limit.</ p >
232+ ) : (
233+ < p className = "mt-2 text-sm text-muted-foreground text-center" >
234+ { isDragActive ? 'Drop files or a folder here...' : 'Drag & drop files or a folder' }
235+ </ p >
236+ ) }
161237 < div className = 'mt-4 flex flex-col sm:flex-row gap-2 w-full' >
162238 < Button type = "button" variant = "outline" onClick = { ( ) => fileInputRef . current ?. click ( ) } disabled = { isLimitReached } className = 'w-full' >
163239 < FileUp className = "mr-2 h-4 w-4" /> Select Files
@@ -166,7 +242,6 @@ export default function SubmissionUploadForm({ problemId, uploadLimits }: Submis
166242 < FolderUp className = "mr-2 h-4 w-4" /> Select Folder
167243 </ Button >
168244 </ div >
169-
170245 < p className = "text-xs text-muted-foreground mt-4" >
171246 Max { uploadLimits . max_num > 0 ? `${ uploadLimits . max_num } files` : 'unlimited files' } , up to { uploadLimits . max_size > 0 ? `${ uploadLimits . max_size } MB total` : 'unlimited size' } .
172247 </ p >
@@ -177,8 +252,8 @@ export default function SubmissionUploadForm({ problemId, uploadLimits }: Submis
177252 < h4 className = "font-semibold" > Selected files:</ h4 >
178253 < ul className = "space-y-1 bg-muted p-3 rounded-md max-h-48 overflow-y-auto" >
179254 { files . map ( ( file ) => {
180- const displayPath = ( file as FileWithPath ) . path || ( file as any ) . webkitRelativePath || file . name ;
181- return (
255+ const displayPath = ( file as FileWithPath ) . path || ( file as any ) . webkitRelativePath || file . name ;
256+ return (
182257 < li key = { `${ displayPath } -${ file . lastModified } ` } className = "flex items-center justify-between text-sm" >
183258 < span className = "flex items-center gap-2 truncate" >
184259 < FileIcon className = "h-4 w-4 shrink-0" />
@@ -189,8 +264,8 @@ export default function SubmissionUploadForm({ problemId, uploadLimits }: Submis
189264 < X className = "h-4 w-4" />
190265 </ Button >
191266 </ li >
192- ) ;
193- } ) }
267+ ) ;
268+ } ) }
194269 </ ul >
195270 </ div >
196271 ) }
0 commit comments