samar m Claude Sonnet 4.6 commited on
Commit
88a0730
·
1 Parent(s): 5949146

feat: Upload page with drag-and-drop UploadZone and CSV format guide

Browse files
frontend/src/components/instructor/UploadZone.tsx CHANGED
@@ -1,3 +1,47 @@
1
- export default function UploadZone() {
2
- return <div>UploadZone</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  }
 
1
+ import { useRef, useState } from 'react'
2
+
3
+ interface Props {
4
+ onFile: (file: File) => void
5
+ uploading: boolean
6
+ }
7
+
8
+ export default function UploadZone({ onFile, uploading }: Props) {
9
+ const inputRef = useRef<HTMLInputElement>(null)
10
+ const [dragging, setDragging] = useState(false)
11
+
12
+ function handleDrop(e: React.DragEvent) {
13
+ e.preventDefault()
14
+ setDragging(false)
15
+ const file = e.dataTransfer.files[0]
16
+ if (file) onFile(file)
17
+ }
18
+
19
+ function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
20
+ const file = e.target.files?.[0]
21
+ if (file) onFile(file)
22
+ }
23
+
24
+ return (
25
+ <div
26
+ onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
27
+ onDragLeave={() => setDragging(false)}
28
+ onDrop={handleDrop}
29
+ onClick={() => inputRef.current?.click()}
30
+ className={`border-2 border-dashed rounded-xl p-12 text-center cursor-pointer transition-colors select-none ${
31
+ dragging ? 'border-indigo-500 bg-indigo-950' : 'border-gray-700 hover:border-gray-600'
32
+ } ${uploading ? 'opacity-50 pointer-events-none' : ''}`}
33
+ >
34
+ <input
35
+ ref={inputRef}
36
+ type="file"
37
+ accept=".csv"
38
+ className="hidden"
39
+ onChange={handleChange}
40
+ />
41
+ <p className="text-gray-300 font-medium">
42
+ {uploading ? 'Uploading...' : 'Drop your CSV here or click to browse'}
43
+ </p>
44
+ <p className="text-gray-500 text-sm mt-1">.csv files only</p>
45
+ </div>
46
+ )
47
  }
frontend/src/pages/Upload.tsx CHANGED
@@ -1,3 +1,80 @@
 
 
 
 
 
 
1
  export default function Upload() {
2
- return <div>Upload</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  }
 
1
+ import { useState } from 'react'
2
+ import { useNavigate, useSearchParams } from 'react-router-dom'
3
+ import Navbar from '../components/shared/Navbar'
4
+ import UploadZone from '../components/instructor/UploadZone'
5
+ import { uploadCSV } from '../api/upload'
6
+
7
  export default function Upload() {
8
+ const navigate = useNavigate()
9
+ const [searchParams] = useSearchParams()
10
+ const topicId = searchParams.get('topic_id') ?? ''
11
+ const topicName = searchParams.get('topic_name') ?? 'Unknown topic'
12
+ const [result, setResult] = useState<{ inserted: number } | null>(null)
13
+ const [error, setError] = useState('')
14
+ const [uploading, setUploading] = useState(false)
15
+
16
+ async function handleFile(file: File) {
17
+ if (!topicId) {
18
+ setError('Missing topic ID — go back to the dashboard.')
19
+ return
20
+ }
21
+ setError('')
22
+ setUploading(true)
23
+ try {
24
+ const res = await uploadCSV(topicId, file)
25
+ setResult(res)
26
+ } catch (err) {
27
+ setError(err instanceof Error ? err.message : 'Upload failed')
28
+ } finally {
29
+ setUploading(false)
30
+ }
31
+ }
32
+
33
+ return (
34
+ <div className="min-h-screen bg-gray-950">
35
+ <Navbar />
36
+ <div className="max-w-2xl mx-auto px-6 py-8">
37
+ <button
38
+ onClick={() => navigate('/instructor/dashboard')}
39
+ className="text-sm text-gray-400 hover:text-white mb-6 inline-flex items-center gap-1 transition-colors"
40
+ >
41
+ ← Back to dashboard
42
+ </button>
43
+
44
+ <h1 className="text-2xl font-semibold text-white mb-1">Upload questions</h1>
45
+ <p className="text-gray-400 text-sm mb-6">
46
+ Topic: <span className="text-indigo-400">{topicName}</span>
47
+ </p>
48
+
49
+ {result ? (
50
+ <div className="bg-green-950 border border-green-800 rounded-xl p-8 text-center">
51
+ <p className="text-green-300 text-lg font-semibold">
52
+ ✓ {result.inserted} questions uploaded
53
+ </p>
54
+ <button
55
+ onClick={() => navigate('/instructor/dashboard')}
56
+ className="mt-4 text-sm text-gray-400 hover:text-white transition-colors"
57
+ >
58
+ Back to dashboard
59
+ </button>
60
+ </div>
61
+ ) : (
62
+ <>
63
+ <UploadZone onFile={handleFile} uploading={uploading} />
64
+ {error && <p className="text-red-400 text-sm mt-3">{error}</p>}
65
+ <div className="mt-6 bg-gray-900 border border-gray-800 rounded-lg p-4">
66
+ <p className="text-gray-400 text-sm font-medium mb-2">CSV format</p>
67
+ <pre className="text-xs text-gray-500 font-mono whitespace-pre">{`question_text,difficulty\nWhat is Python?,easy\nExplain the GIL,hard\nWhat is a decorator?,medium`}</pre>
68
+ <p className="text-gray-500 text-xs mt-2">
69
+ Difficulty: <code className="text-gray-400">easy</code> |{' '}
70
+ <code className="text-gray-400">medium</code> |{' '}
71
+ <code className="text-gray-400">hard</code>{' '}
72
+ (defaults to <code className="text-gray-400">medium</code> if missing or invalid)
73
+ </p>
74
+ </div>
75
+ </>
76
+ )}
77
+ </div>
78
+ </div>
79
+ )
80
  }