Assignment Submission Dropzone
blockeducationUniversity coursework submission portal with plagiarism check indicator, grading rubric, and deadline timer.
Installation
$pnpm dlx uipkge-ng@latest add assignment-submission-dropzone$npx uipkge-ng@latest add assignment-submission-dropzone$yarn dlx uipkge-ng@latest add assignment-submission-dropzone$bunx uipkge-ng@latest add assignment-submission-dropzoneManifest:
https://uipkge.dev/r/angular/assignment-submission-dropzone.jsonInstalls to:src/app/components/blocks/assignment-submission-dropzone/Variants
Loading interactive previews…
Loading interactive previews…
Schema
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[]
}Files installed (2)
src/app/components/blocks/assignment-submission-dropzone/assignment-submission-dropzone.component.ts26.7 kB
// University coursework submission portal with plagiarism check indicator, grading rubric, // and deadline timer. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, Input, ViewChild, inject, signal, } from '@angular/core' import { Award, BookOpen, CheckCircle2, Clock, FileArchive, GraduationCap, Info, LucideAngularModule, RotateCcw, Save, Send, ShieldAlert, ShieldCheck, Timer, Trash2, UploadCloud, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTextareaComponent } from '@/ui/textarea/textarea.component' 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[] } 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 }, ], }, ] const DEFAULT_FILE: AttachedFile = { name: 'distributed_system_v1.zip', size: '18.4 MB', uploadedAt: 'Aug 24, 14:22 PST', similarity: 0.8, status: 'Ready to submit', } const DEFAULT_NOTES = 'Included benchmark logs in the /docs folder. All 12 distributed Raft consensus nodes passed the Chaos Mesh partition tests under 500ms latency simulation.' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-assignment-submission-dropzone, [ui-assignment-submission-dropzone]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiSeparatorComponent, UiTextareaComponent, ], template: ` <div data-slot="assignment-submission-dropzone" [class]="rootClass"> <!-- Assignment Header Card --> <div ui-card class="shadow-xs"> <div ui-card-content class="p-6"> <div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-2"> <div class="flex flex-wrap items-center gap-2"> <span ui-badge wrap variant="outline" class="gap-1.5 font-medium"> <lucide-icon [img]="GraduationCap" class="text-primary size-3.5" /> CS 401 · Advanced Distributed Systems </span> <span ui-badge wrap variant="secondary" class="gap-1 text-xs"> <lucide-icon [img]="Award" class="text-muted-foreground size-3" /> 100 Points Possible </span> </div> <h1 class="text-foreground text-xl font-semibold tracking-tight sm:text-2xl"> Final Project: Distributed Microservices Architecture </h1> <p class="text-muted-foreground text-sm"> Term: Fall 2026 · Instructor: Prof. Elena Rostova · Stanford School of Engineering </p> </div> <!-- Deadline Timer & Badges --> <div class="flex flex-col items-start gap-3 sm:flex-row sm:items-center lg:flex-col lg:items-end"> <span ui-badge wrap variant="warning" class="gap-1.5 px-3 py-1 text-xs font-medium"> <lucide-icon [img]="Clock" class="size-3.5" /> Due Friday, Aug 28 at 23:59 PST · 4 Days Left </span> <div class="border-border bg-muted/40 flex items-center gap-2 rounded-lg border px-3 py-1.5 shadow-xs"> <lucide-icon [img]="Timer" class="text-muted-foreground size-4 shrink-0" /> <div class="flex items-center gap-1.5 text-xs font-medium"> <div class="flex items-baseline gap-0.5"> <span class="text-foreground font-semibold">04</span> <span class="text-muted-foreground text-xs">d</span> </div> <span class="text-muted-foreground">:</span> <div class="flex items-baseline gap-0.5"> <span class="text-foreground font-semibold">07</span> <span class="text-muted-foreground text-xs">h</span> </div> <span class="text-muted-foreground">:</span> <div class="flex items-baseline gap-0.5"> <span class="text-foreground font-semibold">32</span> <span class="text-muted-foreground text-xs">m</span> </div> <span class="text-muted-foreground">:</span> <div class="flex items-baseline gap-0.5"> <span class="text-foreground font-semibold">15</span> <span class="text-muted-foreground text-xs">s</span> </div> </div> </div> </div> </div> </div> </div> <!-- Submitted Confirmation Banner --> @if (isSubmitted()) { <div class="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 class="flex items-start gap-3"> <div class="bg-success/20 text-success flex size-9 shrink-0 items-center justify-center rounded-full"> <lucide-icon [img]="CheckCircle2" class="size-5" /> </div> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <h2 class="text-base font-semibold">Coursework Submitted Successfully</h2> <span ui-badge wrap variant="success" class="text-xs">Receipt #SUB-CS401-2026-98124</span> </div> <p class="text-muted-foreground text-xs sm:text-sm"> Submission timestamp: August 24, 2026 at 14:25 PST · Hash: <code class="font-mono text-xs">sha256:4a8b...7f12</code> </p> </div> </div> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" (click)="handleResetSubmission()"> <lucide-icon [img]="RotateCcw" class="size-3.5" /> Edit Submission </button> </div> </div> } <!-- 2-Column Submission Layout --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- Left Column: Submission & Dropzone Form --> <div class="space-y-6 lg:col-span-7 xl:col-span-7"> <div ui-card class="shadow-xs"> <div ui-card-header> <div class="flex items-center justify-between"> <div class="space-y-1"> <h3 ui-card-title class="text-lg font-semibold">Submission & File Upload</h3> <p ui-card-description> Upload your completed project archive, code repository bundle, and accompanying documentation. </p> </div> <span ui-badge wrap variant="outline" class="text-xs">Attempt 1 of 3</span> </div> </div> <div ui-card-content class="space-y-5"> <!-- Submission Instructions & Constraints --> <div class="border-border bg-muted/40 flex items-start gap-3 rounded-lg border p-3.5 text-xs shadow-xs"> <lucide-icon [img]="Info" class="text-primary mt-0.5 size-4 shrink-0" /> <div class="space-y-0.5"> <p class="text-foreground font-medium">Accepted formats: PDF, ZIP, TAR.GZ · Max file size: 50 MB</p> <p class="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 #fileInput type="file" class="hidden" accept=".zip,.tar.gz,.tar,.pdf" (change)="handleFileInput($event)" /> <div [class]="dropzoneClass" (dragover)="handleDragOver($event)" (dragleave)="handleDragLeave($event)" (drop)="handleDrop($event)" > <div class="bg-primary/10 text-primary flex size-12 items-center justify-center rounded-full transition-transform duration-200 group-hover:scale-105" > <lucide-icon [img]="UploadCloud" class="size-6" /> </div> <div class="mt-3 space-y-1"> <p class="text-foreground text-sm font-medium">Drag and drop your project archive here</p> <p class="text-muted-foreground text-xs">or select a file directly from your local filesystem</p> </div> <div class="mt-4 flex items-center gap-2"> <button ui-button variant="outline" size="sm" type="button" (click)="triggerBrowse()"> Browse files </button> </div> <div class="text-muted-foreground mt-4 flex items-center gap-1.5 text-xs"> <span class="border-border bg-muted rounded px-1.5 py-0.5 font-mono text-xs">.zip</span> <span class="border-border bg-muted rounded px-1.5 py-0.5 font-mono text-xs">.tar.gz</span> <span class="border-border bg-muted rounded px-1.5 py-0.5 font-mono text-xs">.pdf</span> </div> </div> </div> <!-- Attached Files List --> <div class="space-y-2"> <div class="flex items-center justify-between"> <span class="text-foreground text-xs font-semibold tracking-wider uppercase"> Attached Files ({{ file() ? '1' : '0' }} / 1) </span> @if (!file()) { <span class="text-muted-foreground text-xs">No file attached</span> } </div> @if (file(); as attached) { <div class="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 class="flex min-w-0 items-center gap-3"> <div class="bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-md" > <lucide-icon [img]="FileArchive" class="size-5" /> </div> <div class="min-w-0 space-y-0.5"> <p class="text-foreground truncate text-sm font-medium">{{ attached.name }}</p> <p class="text-muted-foreground text-xs">{{ attached.size }} · {{ attached.uploadedAt }}</p> </div> </div> <div class="flex items-center gap-2 self-end sm:self-center"> <span ui-badge wrap variant="secondary" class="text-xs">{{ attached.size }}</span> <span ui-badge wrap variant="outline" class="border-success/30 text-success text-xs font-medium"> {{ attached.status }} </span> <button ui-button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-destructive" title="Remove file" aria-label="Remove attached file" (click)="removeFile()" > <lucide-icon [img]="Trash2" class="size-4" /> </button> </div> </div> } @else { <div class="border-border bg-muted/20 flex items-center justify-between rounded-lg border border-dashed p-3 text-xs" > <span class="text-muted-foreground">Sample archive cleared.</span> <button ui-button variant="ghost" size="xs" class="text-primary text-xs" (click)="restoreSampleFile()" > Restore default file </button> </div> } </div> <!-- Plagiarism & Similarity Pre-scan Indicator --> @if (file(); as scanned) { <div [class]="scanPanelClass"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-2"> @if (isCleanSimilarity()) { <lucide-icon [img]="ShieldCheck" class="text-success size-5 shrink-0" /> } @else { <lucide-icon [img]="ShieldAlert" class="text-warning size-5 shrink-0" /> } <div> <h3 class="text-foreground text-sm font-semibold">Academic Integrity & Plagiarism Pre-Scan</h3> <p class="text-muted-foreground text-xs"> Scanned against 14.2M academic repositories & public open-source codebases </p> </div> </div> <span ui-badge wrap [variant]="isCleanSimilarity() ? 'success' : 'warning'" class="gap-1 self-start text-xs font-medium sm:self-auto" > <lucide-icon [img]="ShieldCheck" class="size-3" /> {{ scanned.similarity }}% Similarity · {{ isCleanSimilarity() ? 'Clean' : 'Needs Review' }} </span> </div> <div class="space-y-1.5 pt-1"> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">Overall similarity index</span> <span class="text-foreground font-mono font-medium"> {{ scanned.similarity }}% (Max threshold: 15.0%) </span> </div> <ui-progress [value]="scanned.similarity * 6.66" class="h-1.5" /> </div> <div class="grid grid-cols-1 gap-2 pt-1 text-xs sm:grid-cols-3"> <div class="border-border/60 bg-card/60 rounded border p-2"> <span class="text-muted-foreground block text-xs">Internet Sources</span> <span class="text-foreground font-medium">0.0% match</span> </div> <div class="border-border/60 bg-card/60 rounded border p-2"> <span class="text-muted-foreground block text-xs">Peer Submissions</span> <span class="text-foreground font-medium">0.0% match</span> </div> <div class="border-border/60 bg-card/60 rounded border p-2"> <span class="text-muted-foreground block text-xs">Standard Boilerplate</span> <span class="text-foreground font-medium">{{ scanned.similarity }}% (Apache 2.0)</span> </div> </div> </div> } <!-- Student Submission Notes Textarea --> <div class="space-y-2"> <label for="angular-submission-notes" class="text-foreground text-sm font-medium"> Student Submission Notes & Execution Instructions </label> <ui-textarea id="angular-submission-notes" [value]="notes()" (valueChange)="notes.set($event)" [rows]="3" placeholder="Included benchmark logs in the /docs folder..." class="text-sm" /> <p class="text-muted-foreground text-xs"> Provide notes on environment configurations, docker flags, or benchmark reproducibility steps. </p> </div> </div> <div ui-card-footer class="border-border flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between" > <div class="text-muted-foreground flex items-center gap-2 text-xs"> <span class="bg-success size-2 rounded-full"></span> @if (isDraftSaved()) { <span class="text-success font-medium">Draft saved successfully!</span> } @else { <span>Draft auto-saved 2 mins ago · rev 3</span> } </div> <div class="flex w-full flex-wrap items-center gap-2.5 sm:w-auto"> <button ui-button variant="outline" size="sm" class="flex-1 sm:flex-initial" [disabled]="isSubmitted()" (click)="handleSaveDraft()" > <lucide-icon [img]="Save" class="size-3.5" /> Save Draft </button> <button ui-button variant="default" size="sm" class="flex-1 sm:flex-initial" [disabled]="!file() || isSubmitted()" (click)="handleSubmit()" > <lucide-icon [img]="Send" class="size-3.5" /> Submit Assignment </button> </div> </div> </div> </div> <!-- Right Column: Grading Rubric & Assessment Criteria --> <div class="space-y-6 lg:col-span-5 xl:col-span-5"> <div ui-card class="shadow-xs"> <div ui-card-header> <div class="flex items-center justify-between"> <div class="space-y-1"> <h3 ui-card-title class="flex items-center gap-2 text-lg font-semibold"> <lucide-icon [img]="BookOpen" class="text-primary size-5" /> Grading Rubric </h3> <p ui-card-description>100 Points Total · Evaluated against course criteria</p> </div> <span ui-badge wrap variant="outline" class="font-mono text-xs">Pass: 70 pts</span> </div> </div> <div ui-card-content class="space-y-5"> @for (category of rubricCategories; track category.id; let last = $last) { <div class="space-y-3"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <h4 class="text-foreground text-sm font-medium">{{ category.title }}</h4> <span ui-badge wrap variant="secondary" class="font-mono text-xs"> {{ category.points }} pts ({{ category.percentage }}%) </span> </div> <ui-progress [value]="category.percentage" class="h-1.5" /> </div> <ul class="border-border/60 bg-muted/20 space-y-2 rounded-lg border p-2.5 text-xs"> @for (criterion of category.criteria; track criterion.label) { <li class="flex items-start justify-between gap-2"> <div class="flex min-w-0 items-start gap-1.5"> <span class="text-primary font-bold">·</span> <span class="text-muted-foreground">{{ criterion.label }}</span> </div> <span class="text-foreground shrink-0 font-mono font-medium">{{ criterion.points }} pts</span> </li> } </ul> @if (!last) { <div ui-separator class="mt-4"></div> } </div> } </div> <div ui-card-footer class="border-border bg-muted/30 text-muted-foreground flex flex-col gap-2 rounded-b-xl border-t p-4 text-xs" > <div class="flex items-start gap-2"> <lucide-icon [img]="Info" class="text-muted-foreground mt-0.5 size-4 shrink-0" /> <p> <strong class="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> </div> </div> </div> </div> </div> `, }) export class UiAssignmentSubmissionDropzoneComponent { protected readonly Award = Award protected readonly BookOpen = BookOpen protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly FileArchive = FileArchive protected readonly GraduationCap = GraduationCap protected readonly Info = Info protected readonly RotateCcw = RotateCcw protected readonly Save = Save protected readonly Send = Send protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck protected readonly Timer = Timer protected readonly Trash2 = Trash2 protected readonly UploadCloud = UploadCloud protected readonly rubricCategories = defaultRubricCategories @ViewChild('fileInput') private fileInput?: ElementRef<HTMLInputElement> @Input() set initialFile(v: AttachedFile | null | undefined) { this.file.set(v === undefined ? { ...DEFAULT_FILE } : v ? { ...v } : null) } @Input() set initialNotes(v: string) { if (v !== undefined) this.notes.set(v) } @Input() set initialSubmitted(v: boolean) { this.isSubmitted.set(!!v) } @Input('class') className?: string readonly file = signal<AttachedFile | null>({ ...DEFAULT_FILE }) readonly notes = signal(DEFAULT_NOTES) readonly isSubmitted = signal(false) readonly isDraftSaved = signal(false) readonly isDragging = signal(false) private draftTimer: ReturnType<typeof setTimeout> | undefined constructor() { inject(DestroyRef).onDestroy(() => clearTimeout(this.draftTimer)) } get rootClass(): string { return cn('space-y-6', this.className) } get dropzoneClass(): string { return cn( 'group relative flex flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 text-center transition-colors duration-200', this.isDragging() ? 'border-primary bg-primary/5' : 'border-border bg-muted/10 hover:border-primary/50 hover:bg-muted/20', ) } get scanPanelClass(): string { return cn( 'space-y-3 rounded-lg border p-4 shadow-xs transition-colors', this.isCleanSimilarity() ? 'border-success/30 bg-success/5 dark:bg-success/10' : 'border-warning/30 bg-warning/5 dark:bg-warning/10', ) } isCleanSimilarity(): boolean { const f = this.file() return !f || f.similarity < 10 } triggerBrowse(): void { this.fileInput?.nativeElement.click() } handleFileInput(event: Event): void { const input = event.target as HTMLInputElement const picked = input.files?.[0] if (picked) this.attachPickedFile(picked.name, picked.size) } handleDrop(event: DragEvent): void { event.preventDefault() this.isDragging.set(false) const picked = event.dataTransfer?.files?.[0] if (picked) this.attachPickedFile(picked.name, picked.size) } handleDragOver(event: DragEvent): void { event.preventDefault() this.isDragging.set(true) } handleDragLeave(event: DragEvent): void { event.preventDefault() this.isDragging.set(false) } removeFile(): void { this.file.set(null) } restoreSampleFile(): void { this.file.set({ ...DEFAULT_FILE }) } handleSaveDraft(): void { this.isDraftSaved.set(true) clearTimeout(this.draftTimer) this.draftTimer = setTimeout(() => this.isDraftSaved.set(false), 3000) } handleSubmit(): void { if (!this.file()) return this.isSubmitted.set(true) } handleResetSubmission(): void { this.isSubmitted.set(false) } private attachPickedFile(name: string, sizeBytes: number): void { this.file.set({ name, size: `${(sizeBytes / (1024 * 1024)).toFixed(1)} MB`, uploadedAt: 'Just now', similarity: 0.8, status: 'Ready to submit', }) } }src/app/components/blocks/assignment-submission-dropzone/index.ts0.2 kB
export { UiAssignmentSubmissionDropzoneComponent, type AttachedFile, type RubricCategory, type RubricCriterion, } from './assignment-submission-dropzone.component'
Raw manifest:https://uipkge.dev/r/angular/assignment-submission-dropzone.json