Menu

Change language

GitHub repo

Assignment Submission Dropzone

blockeducation

University coursework submission portal with plagiarism check indicator, grading rubric, and deadline timer.

Installation

$npx uipkge-ng@latest add assignment-submission-dropzone
Manifest:https://uipkge.dev/r/angular/assignment-submission-dropzone.jsonInstalls to:src/app/components/blocks/assignment-submission-dropzone/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

AttachedFile
interface AttachedFile {
  name: string
  size: string
  uploadedAt: string
  similarity: number
  status: string
}
RubricCriterion
interface RubricCriterion {
  label: string
  points: number
}
RubricCategory
interface 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

Raw manifest:https://uipkge.dev/r/angular/assignment-submission-dropzone.json