
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
University coursework submission portal with plagiarism check indicator, grading rubric, and deadline timer.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/assignment-submission-dropzone.json$npx shadcn@latest add https://uipkge.dev/r/react/assignment-submission-dropzone.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/assignment-submission-dropzone.json$bunx shadcn@latest add https://uipkge.dev/r/react/assignment-submission-dropzone.jsonnpx shadcn@latest add @uipkge-react/assignment-submission-dropzoneInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
initialFile | AttachedFile | null | — | optional |
initialNotes | string | — | optional |
initialSubmitted | boolean | — | optional |
className | string | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
AttachedFileinterface AttachedFile {
name: string
size: string
uploadedAt: string
similarity: number
status: string
}RubricCriterioninterface RubricCriterion {
label: string
points: number
}RubricCategoryinterface RubricCategory {
id: string
title: string
points: number
percentage: number
criteria: RubricCriterion[]
}'use client'
import * as React from 'react'
import {
Award,
BookOpen,
CheckCircle2,
Clock,
FileArchive,
GraduationCap,
Info,
RotateCcw,
Save,
Send,
ShieldAlert,
ShieldCheck,
Timer,
Trash2,
UploadCloud,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea'
export interface AttachedFile {
name: string
size: string
uploadedAt: string
similarity: number
status: string
}
export interface RubricCriterion {
label: string
points: number
}
export interface RubricCategory {
id: string
title: string
points: number
percentage: number
criteria: RubricCriterion[]
}
export interface AssignmentSubmissionDropzoneProps {
initialFile?: AttachedFile | null
initialNotes?: string
initialSubmitted?: boolean
className?: string
}
const defaultRubricCategories: RubricCategory[] = [
{
id: 'architecture',
title: 'Architecture & Fault Tolerance',
points: 40,
percentage: 40,
criteria: [
{ label: 'Consensus algorithm implementation (Raft / Paxos) & partition resilience', points: 20 },
{ label: 'Service discovery, dynamic load balancing, & circuit breaking', points: 15 },
{ label: 'Data replication & linearizable consistency guarantees', points: 5 },
],
},
{
id: 'code-quality',
title: 'Code Quality & Test Coverage',
points: 30,
percentage: 30,
criteria: [
{ label: 'Automated unit, fuzz, & integration test suite (>85% coverage)', points: 15 },
{ label: 'Idiomatic concurrency patterns, memory safety, clean modularity', points: 10 },
{ label: 'Reproducible CI/CD pipeline automation & static analysis pass', points: 5 },
],
},
{
id: 'performance',
title: 'Performance Benchmarks',
points: 20,
percentage: 20,
criteria: [
{ label: 'Sustained throughput under load (>15,000 req/sec benchmark)', points: 10 },
{ label: 'P99 latency SLA (<25ms under 50% simulated node failure)', points: 10 },
],
},
{
id: 'documentation',
title: 'Documentation & API Specs',
points: 10,
percentage: 10,
criteria: [
{ label: 'OpenAPI 3.1 & gRPC Protobuf schema specifications', points: 5 },
{ label: 'System architecture design doc & production deployment runbook', points: 5 },
],
},
]
export function AssignmentSubmissionDropzone({
initialFile = {
name: 'distributed_system_v1.zip',
size: '18.4 MB',
uploadedAt: 'Aug 24, 14:22 PST',
similarity: 0.8,
status: 'Ready to submit',
},
initialNotes = 'Included benchmark logs in the /docs folder. All 12 distributed Raft consensus nodes passed the Chaos Mesh partition tests under 500ms latency simulation.',
initialSubmitted = false,
className,
}: AssignmentSubmissionDropzoneProps) {
const [file, setFile] = React.useState<AttachedFile | null>(initialFile)
const [notes, setNotes] = React.useState(initialNotes)
const [isSubmitted, setIsSubmitted] = React.useState(initialSubmitted)
const [isDraftSaved, setIsDraftSaved] = React.useState(false)
const [isDragging, setIsDragging] = React.useState(false)
const fileInputRef = React.useRef<HTMLInputElement | null>(null)
const isCleanSimilarity = !file || file.similarity < 10
const triggerBrowse = () => {
fileInputRef.current?.click()
}
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const f = e.target.files[0]
setFile({
name: f.name,
size: `${(f.size / (1024 * 1024)).toFixed(1)} MB`,
uploadedAt: 'Just now',
similarity: 0.8,
status: 'Ready to submit',
})
}
}
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
setIsDragging(false)
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
const f = e.dataTransfer.files[0]
setFile({
name: f.name,
size: `${(f.size / (1024 * 1024)).toFixed(1)} MB`,
uploadedAt: 'Just now',
similarity: 0.8,
status: 'Ready to submit',
})
}
}
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
setIsDragging(true)
}
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
setIsDragging(false)
}
const removeFile = () => {
setFile(null)
}
const restoreSampleFile = () => {
setFile({
name: 'distributed_system_v1.zip',
size: '18.4 MB',
uploadedAt: 'Aug 24, 14:22 PST',
similarity: 0.8,
status: 'Ready to submit',
})
}
const handleSaveDraft = () => {
setIsDraftSaved(true)
setTimeout(() => {
setIsDraftSaved(false)
}, 3000)
}
const handleSubmit = () => {
if (!file) return
setIsSubmitted(true)
}
const handleResetSubmission = () => {
setIsSubmitted(false)
}
return (
<div data-slot="assignment-submission-dropzone" className={cn('space-y-6', className)}>
{/* Assignment Header Card */}
<Card className="shadow-xs">
<CardContent className="p-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Badge wrap variant="outline" className="gap-1.5 font-medium">
<GraduationCap className="text-primary size-3.5" aria-hidden="true" />
CS 401 · Advanced Distributed Systems
</Badge>
<Badge wrap variant="secondary" className="gap-1 text-xs">
<Award className="text-muted-foreground size-3" aria-hidden="true" />
100 Points Possible
</Badge>
</div>
<h1 className="text-foreground text-xl font-semibold tracking-tight sm:text-2xl">
Final Project: Distributed Microservices Architecture
</h1>
<p className="text-muted-foreground text-sm">
Term: Fall 2026 · Instructor: Prof. Elena Rostova · Stanford School of Engineering
</p>
</div>
{/* Deadline Timer & Badges */}
<div className="flex flex-col items-start gap-3 sm:flex-row sm:items-center lg:flex-col lg:items-end">
<Badge wrap variant="warning" className="gap-1.5 px-3 py-1 text-xs font-medium">
<Clock className="size-3.5" aria-hidden="true" />
Due Friday, Aug 28 at 23:59 PST · 4 Days Left
</Badge>
{/* Countdown blocks */}
<div className="border-border bg-muted/40 flex items-center gap-2 rounded-lg border px-3 py-1.5 shadow-xs">
<Timer className="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
<div className="flex items-center gap-1.5 text-xs font-medium">
<div className="flex items-baseline gap-0.5">
<span className="text-foreground font-semibold">04</span>
<span className="text-muted-foreground text-xs">d</span>
</div>
<span className="text-muted-foreground">:</span>
<div className="flex items-baseline gap-0.5">
<span className="text-foreground font-semibold">07</span>
<span className="text-muted-foreground text-xs">h</span>
</div>
<span className="text-muted-foreground">:</span>
<div className="flex items-baseline gap-0.5">
<span className="text-foreground font-semibold">32</span>
<span className="text-muted-foreground text-xs">m</span>
</div>
<span className="text-muted-foreground">:</span>
<div className="flex items-baseline gap-0.5">
<span className="text-foreground font-semibold">15</span>
<span className="text-muted-foreground text-xs">s</span>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Submitted Confirmation Banner */}
{isSubmitted && (
<div
className="border-success/30 bg-success/10 text-foreground flex flex-col gap-4 rounded-xl border p-5 sm:flex-row sm:items-center sm:justify-between"
role="status"
>
<div className="flex items-start gap-3">
<div className="bg-success/20 text-success flex size-9 shrink-0 items-center justify-center rounded-full">
<CheckCircle2 className="size-5" aria-hidden="true" />
</div>
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-base font-semibold">Coursework Submitted Successfully</h2>
<Badge wrap variant="success" className="text-xs">
Receipt #SUB-CS401-2026-98124
</Badge>
</div>
<p className="text-muted-foreground text-xs sm:text-sm">
Submission timestamp: August 24, 2026 at 14:25 PST · Hash:{' '}
<code className="font-mono text-xs">sha256:4a8b...7f12</code>
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleResetSubmission}>
<RotateCcw className="size-3.5" aria-hidden="true" />
Edit Submission
</Button>
</div>
</div>
)}
{/* 2-Column Submission Layout */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12">
{/* Left Column: Submission & Dropzone Form */}
<div className="space-y-6 lg:col-span-7 xl:col-span-7">
<Card className="shadow-xs">
<CardHeader>
<div className="flex items-center justify-between">
<div className="space-y-1">
<CardTitle className="text-lg font-semibold">Submission & File Upload</CardTitle>
<CardDescription>
Upload your completed project archive, code repository bundle, and accompanying documentation.
</CardDescription>
</div>
<Badge wrap variant="outline" className="text-xs">
Attempt 1 of 3
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-5">
{/* Submission Instructions & Constraints */}
<div className="border-border bg-muted/40 flex items-start gap-3 rounded-lg border p-3.5 text-xs shadow-xs">
<Info className="text-primary mt-0.5 size-4 shrink-0" aria-hidden="true" />
<div className="space-y-0.5">
<p className="text-foreground font-medium">
Accepted formats: PDF, ZIP, TAR.GZ · Max file size: 50 MB
</p>
<p className="text-muted-foreground">
Ensure all Docker Compose manifests, benchmarking scripts, and unit tests are included in the
archive root.
</p>
</div>
</div>
{/* File Dropzone */}
<div>
<input
ref={fileInputRef}
type="file"
className="hidden"
accept=".zip,.tar.gz,.tar,.pdf"
onChange={handleFileInput}
/>
<div
className={cn(
'group relative flex flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 text-center transition-colors duration-200',
isDragging
? 'border-primary bg-primary/5'
: 'border-border bg-muted/10 hover:border-primary/50 hover:bg-muted/20',
)}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<div className="bg-primary/10 text-primary flex size-12 items-center justify-center rounded-full transition-transform duration-200 group-hover:scale-105">
<UploadCloud className="size-6" aria-hidden="true" />
</div>
<div className="mt-3 space-y-1">
<p className="text-foreground text-sm font-medium">Drag and drop your project archive here</p>
<p className="text-muted-foreground text-xs">
or select a file directly from your local filesystem
</p>
</div>
<div className="mt-4 flex items-center gap-2">
<Button variant="outline" size="sm" type="button" onClick={triggerBrowse}>
Browse files
</Button>
</div>
<div className="text-muted-foreground mt-4 flex items-center gap-1.5 text-xs">
<span className="border-border bg-muted rounded px-1.5 py-0.5 font-mono text-xs">.zip</span>
<span className="border-border bg-muted rounded px-1.5 py-0.5 font-mono text-xs">.tar.gz</span>
<span className="border-border bg-muted rounded px-1.5 py-0.5 font-mono text-xs">.pdf</span>
</div>
</div>
</div>
{/* Attached Files List */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-foreground text-xs font-semibold tracking-wider uppercase">
Attached Files ({file ? '1' : '0'} / 1)
</span>
{!file && <span className="text-muted-foreground text-xs">No file attached</span>}
</div>
{/* File Item Card */}
{file ? (
<div className="border-border bg-card flex flex-col gap-3 rounded-lg border p-3.5 shadow-xs transition-colors sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 items-center gap-3">
<div className="bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-md">
<FileArchive className="size-5" aria-hidden="true" />
</div>
<div className="min-w-0 space-y-0.5">
<p className="text-foreground truncate text-sm font-medium">{file.name}</p>
<p className="text-muted-foreground text-xs">
{file.size} · {file.uploadedAt}
</p>
</div>
</div>
<div className="flex items-center gap-2 self-end sm:self-center">
<Badge wrap variant="secondary" className="text-xs">
{file.size}
</Badge>
<Badge wrap variant="outline" className="border-success/30 text-success text-xs font-medium">
{file.status}
</Badge>
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground hover:text-destructive"
title="Remove file"
aria-label="Remove attached file"
onClick={removeFile}
>
<Trash2 className="size-4" aria-hidden="true" />
</Button>
</div>
</div>
) : (
<div className="border-border bg-muted/20 flex items-center justify-between rounded-lg border border-dashed p-3 text-xs">
<span className="text-muted-foreground">Sample archive cleared.</span>
<Button variant="ghost" size="xs" className="text-primary text-xs" onClick={restoreSampleFile}>
Restore default file
</Button>
</div>
)}
</div>
{/* Plagiarism & Similarity Pre-scan Indicator */}
{file && (
<div
className={cn(
'space-y-3 rounded-lg border p-4 shadow-xs transition-colors',
isCleanSimilarity
? 'border-success/30 bg-success/5 dark:bg-success/10'
: 'border-warning/30 bg-warning/5 dark:bg-warning/10',
)}
>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
{isCleanSimilarity ? (
<ShieldCheck className="text-success size-5 shrink-0" aria-hidden="true" />
) : (
<ShieldAlert className="text-warning size-5 shrink-0" aria-hidden="true" />
)}
<div>
<h3 className="text-foreground text-sm font-semibold">
Academic Integrity & Plagiarism Pre-Scan
</h3>
<p className="text-muted-foreground text-xs">
Scanned against 14.2M academic repositories & public open-source codebases
</p>
</div>
</div>
<Badge
wrap
variant={isCleanSimilarity ? 'success' : 'warning'}
className="gap-1 self-start text-xs font-medium sm:self-auto"
>
<ShieldCheck className="size-3" aria-hidden="true" />
{file.similarity}% Similarity · {isCleanSimilarity ? 'Clean' : 'Needs Review'}
</Badge>
</div>
{/* Mini metric progress line */}
<div className="space-y-1.5 pt-1">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">Overall similarity index</span>
<span className="text-foreground font-mono font-medium">
{file.similarity}% (Max threshold: 15.0%)
</span>
</div>
<Progress value={file.similarity * 6.66} className="h-1.5" />
</div>
{/* Integrity breakdown chips */}
<div className="grid grid-cols-1 gap-2 pt-1 text-xs sm:grid-cols-3">
<div className="border-border/60 bg-card/60 rounded border p-2">
<span className="text-muted-foreground block text-xs">Internet Sources</span>
<span className="text-foreground font-medium">0.0% match</span>
</div>
<div className="border-border/60 bg-card/60 rounded border p-2">
<span className="text-muted-foreground block text-xs">Peer Submissions</span>
<span className="text-foreground font-medium">0.0% match</span>
</div>
<div className="border-border/60 bg-card/60 rounded border p-2">
<span className="text-muted-foreground block text-xs">Standard Boilerplate</span>
<span className="text-foreground font-medium">{file.similarity}% (Apache 2.0)</span>
</div>
</div>
</div>
)}
{/* Student Submission Notes Textarea */}
<div className="space-y-2">
<label htmlFor="react-submission-notes" className="text-foreground text-sm font-medium">
Student Submission Notes & Execution Instructions
</label>
<Textarea
id="react-submission-notes"
value={notes}
onValueChange={setNotes}
rows={3}
placeholder="Included benchmark logs in the /docs folder..."
className="text-sm"
/>
<p className="text-muted-foreground text-xs">
Provide notes on environment configurations, docker flags, or benchmark reproducibility steps.
</p>
</div>
</CardContent>
<CardFooter className="border-border flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<span className="bg-success size-2 rounded-full" />
{isDraftSaved ? (
<span className="text-success font-medium">Draft saved successfully!</span>
) : (
<span>Draft auto-saved 2 mins ago · rev 3</span>
)}
</div>
<div className="flex w-full flex-wrap items-center gap-2.5 sm:w-auto">
<Button
variant="outline"
size="sm"
className="flex-1 sm:flex-initial"
disabled={isSubmitted}
onClick={handleSaveDraft}
>
<Save className="size-3.5" aria-hidden="true" />
Save Draft
</Button>
<Button
variant="default"
size="sm"
className="flex-1 sm:flex-initial"
disabled={!file || isSubmitted}
onClick={handleSubmit}
>
<Send className="size-3.5" aria-hidden="true" />
Submit Assignment
</Button>
</div>
</CardFooter>
</Card>
</div>
{/* Right Column: Grading Rubric & Assessment Criteria */}
<div className="space-y-6 lg:col-span-5 xl:col-span-5">
<Card className="shadow-xs">
<CardHeader>
<div className="flex items-center justify-between">
<div className="space-y-1">
<CardTitle className="flex items-center gap-2 text-lg font-semibold">
<BookOpen className="text-primary size-5" aria-hidden="true" />
Grading Rubric
</CardTitle>
<CardDescription>100 Points Total · Evaluated against course criteria</CardDescription>
</div>
<Badge wrap variant="outline" className="font-mono text-xs">
Pass: 70 pts
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-5">
{/* Rubric Categories */}
{defaultRubricCategories.map((category, index) => (
<div key={category.id} className="space-y-3">
{/* Category Header */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<h4 className="text-foreground text-sm font-medium">{category.title}</h4>
<Badge wrap variant="secondary" className="font-mono text-xs">
{category.points} pts ({category.percentage}%)
</Badge>
</div>
<Progress value={category.percentage} className="h-1.5" />
</div>
{/* Specific Sub-criteria */}
<ul className="border-border/60 bg-muted/20 space-y-2 rounded-lg border p-2.5 text-xs">
{category.criteria.map((criterion, cIndex) => (
<li key={cIndex} className="flex items-start justify-between gap-2">
<div className="flex min-w-0 items-start gap-1.5">
<span className="text-primary font-bold">·</span>
<span className="text-muted-foreground">{criterion.label}</span>
</div>
<span className="text-foreground shrink-0 font-mono font-medium">{criterion.points} pts</span>
</li>
))}
</ul>
{index < defaultRubricCategories.length - 1 && <Separator className="mt-4" />}
</div>
))}
</CardContent>
<CardFooter className="border-border bg-muted/30 text-muted-foreground flex flex-col gap-2 rounded-b-xl border-t p-4 text-xs">
<div className="flex items-start gap-2">
<Info className="text-muted-foreground mt-0.5 size-4 shrink-0" aria-hidden="true" />
<p>
<strong className="text-foreground font-medium">Late Submission Policy:</strong> Deductions of 5%
apply per 24 hours delayed up to a maximum 48-hour grace period.
</p>
</div>
</CardFooter>
</Card>
</div>
</div>
</div>
)
}
export default AssignmentSubmissionDropzone
Raw manifest:https://uipkge.dev/r/react/assignment-submission-dropzone.json