Spaces:
Sleeping
Sleeping
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 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|