Menu

Change language

GitHub repo

Student Gradebook Table

blockeducation

Academic gradebook ledger with course header, weighted assignment categories (homework, midterm, labs, final project), class GPA & enrollment metric cards, overall percentage, letter grade badges, attendance tags, and student action menus.

Installation

$npx uipkge-ng@latest add student-gradebook-table
Manifest:https://uipkge.dev/r/angular/student-gradebook-table.jsonInstalls to:src/app/components/blocks/student-gradebook-table/

Variants

Loading interactive previews…

Schema

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

StudentGradeRecord
interface StudentGradeRecord {
  id: string
  name: string
  studentId: string
  homework: string
  midterm: string
  labProjects: string
  finalProject: string
  overallGrade: string
  letterGrade: LetterGrade
  attendanceRate: string
  absences: number
}

Files installed (2)

  • src/app/components/blocks/student-gradebook-table/student-gradebook-table.component.ts13.7 kB
    // Academic gradebook ledger with course header, weighted assignment categories, class GPA
    // and enrollment metric cards, overall percentage, letter grade badges, attendance tags, and
    // student action menus. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import {
      CalendarCheck,
      ClipboardCheck,
      Download,
      GraduationCap,
      LucideAngularModule,
      Mail,
      MoreHorizontal,
      Pencil,
      Plus,
      User,
      Users,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export type LetterGrade = 'A+' | 'A' | 'A-' | 'B+' | 'B' | 'B-'
    
    export interface StudentGradeRecord {
      id: string
      name: string
      studentId: string
      homework: string
      midterm: string
      labProjects: string
      finalProject: string
      overallGrade: string
      letterGrade: LetterGrade
      attendanceRate: string
      absences: number
    }
    
    const metrics = [
      {
        title: 'Class GPA',
        value: '3.62 / 4.0',
        description: '+0.18 vs departmental target',
        icon: GraduationCap,
        iconClass: 'bg-success/10 text-success',
        valueClass: 'text-success',
      },
      {
        title: 'Total Enrolled Students',
        value: '28 students',
        description: '100% active roster',
        icon: Users,
        iconClass: 'bg-info/10 text-info',
        valueClass: 'text-foreground',
      },
      {
        title: 'Assignments Graded',
        value: '12 / 14 assignments',
        description: '2 pending final submissions',
        icon: ClipboardCheck,
        iconClass: 'bg-chart-1/10 text-chart-1',
        valueClass: 'text-foreground',
      },
      {
        title: 'Class Attendance Rate',
        value: '96.2% average',
        description: '36 lectures & labs logged',
        icon: CalendarCheck,
        iconClass: 'bg-warning/10 text-warning',
        valueClass: 'text-foreground',
      },
    ]
    
    const gradeBadgeConfig: Record<LetterGrade, { class: string }> = {
      'A+': { class: 'bg-success/10 text-success border-success/20 font-semibold' },
      A: { class: 'bg-success/10 text-success border-success/20 font-semibold' },
      'A-': { class: 'bg-success/10 text-success border-success/20 font-semibold' },
      'B+': { class: 'bg-info/10 text-info border-info/20 font-semibold' },
      B: { class: 'bg-warning/10 text-warning border-warning/20 font-semibold' },
      'B-': { class: 'bg-warning/10 text-warning border-warning/20 font-semibold' },
    }
    
    const students: StudentGradeRecord[] = [
      {
        id: 's1',
        name: 'Elena Rostova',
        studentId: '#STU-84920',
        homework: '95.0%',
        midterm: '92.0%',
        labProjects: '98.0%',
        finalProject: '94.0%',
        overallGrade: '94.5%',
        letterGrade: 'A',
        attendanceRate: '100%',
        absences: 0,
      },
      {
        id: 's2',
        name: 'David Chen',
        studentId: '#STU-84921',
        homework: '88.0%',
        midterm: '91.0%',
        labProjects: '86.0%',
        finalProject: '89.0%',
        overallGrade: '88.6%',
        letterGrade: 'B+',
        attendanceRate: '96%',
        absences: 1,
      },
      {
        id: 's3',
        name: 'Sarah Jenkins',
        studentId: '#STU-84922',
        homework: '98.0%',
        midterm: '96.0%',
        labProjects: '100.0%',
        finalProject: '95.0%',
        overallGrade: '97.1%',
        letterGrade: 'A',
        attendanceRate: '100%',
        absences: 0,
      },
      {
        id: 's4',
        name: 'Marcus Vance',
        studentId: '#STU-84923',
        homework: '82.0%',
        midterm: '79.0%',
        labProjects: '85.0%',
        finalProject: '80.0%',
        overallGrade: '81.4%',
        letterGrade: 'B',
        attendanceRate: '92%',
        absences: 2,
      },
      {
        id: 's5',
        name: 'Sofia Rossi',
        studentId: '#STU-84924',
        homework: '91.0%',
        midterm: '88.0%',
        labProjects: '94.0%',
        finalProject: '90.0%',
        overallGrade: '90.6%',
        letterGrade: 'A-',
        attendanceRate: '98%',
        absences: 1,
      },
      {
        id: 's6',
        name: 'Alex Rivera',
        studentId: '#STU-84925',
        homework: '84.0%',
        midterm: '86.0%',
        labProjects: '82.0%',
        finalProject: '85.0%',
        overallGrade: '84.3%',
        letterGrade: 'B',
        attendanceRate: '94%',
        absences: 2,
      },
    ]
    
    function initials(name: string): string {
      return name
        .split(' ')
        .map((part) => part[0])
        .slice(0, 2)
        .join('')
        .toUpperCase()
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-student-gradebook-table, [ui-student-gradebook-table]',
      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,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuTriggerComponent,
        UiTableComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="student-gradebook-table" [class]="rootClass">
          <!-- Header -->
          <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div>
              <div class="flex flex-wrap items-center gap-2.5">
                <h2 class="text-2xl font-bold tracking-tight">CS 301: Advanced Software Systems &amp; Compilers</h2>
                <span ui-badge variant="outline" class="font-mono text-xs">CS-301-B</span>
              </div>
              <div class="text-muted-foreground mt-1.5 flex flex-wrap items-center gap-2 text-sm">
                <span>Fall 2026 · Section B</span>
                <span class="text-foreground inline-flex items-center gap-1 font-medium">
                  Class Average: <span class="tabular-nums">88.4%</span> · Grade B+
                </span>
              </div>
            </div>
            <div class="flex flex-wrap items-center gap-2.5">
              <button ui-button aria-label="Download attachment" variant="outline">
                <lucide-icon [img]="Download" class="size-4" />
                Export Grades CSV
              </button>
              <button ui-button>
                <lucide-icon [img]="Plus" class="size-4" />
                New Grade Entry
              </button>
            </div>
          </div>
    
          <!-- 4 Class Overview Metric Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            @for (metric of metrics; track metric.title) {
              <div ui-card class="shadow-xs">
                <div ui-card-content class="space-y-2 p-5">
                  <div class="flex items-center justify-between">
                    <p class="text-muted-foreground text-sm font-medium">{{ metric.title }}</p>
                    <div [class]="metricIconClass(metric.iconClass)">
                      <lucide-icon [img]="metric.icon" class="size-4" />
                    </div>
                  </div>
                  <div>
                    <p [class]="metricValueClass(metric.valueClass)">{{ metric.value }}</p>
                    <p class="text-muted-foreground mt-1 text-xs">{{ metric.description }}</p>
                  </div>
                </div>
              </div>
            }
          </div>
    
          <!-- Gradebook Table -->
          <div class="bg-card overflow-x-auto rounded-lg border shadow-xs">
            <table ui-table>
              <thead ui-table-header>
                <tr ui-table-row>
                  <th ui-table-head>Student</th>
                  <th ui-table-head class="text-right">Homework (25%)</th>
                  <th ui-table-head class="text-right">Midterm (30%)</th>
                  <th ui-table-head class="text-right">Lab Projects (25%)</th>
                  <th ui-table-head class="text-right">Final Project (20%)</th>
                  <th ui-table-head class="text-right font-semibold">Overall Grade</th>
                  <th ui-table-head class="text-center">Letter Grade</th>
                  <th ui-table-head>Attendance</th>
                  <th ui-table-head class="w-12 text-right">
                    <span class="sr-only">Actions</span>
                  </th>
                </tr>
              </thead>
              <tbody ui-table-body>
                @for (student of students; track student.id) {
                  <tr ui-table-row class="hover:bg-muted/50">
                    <td ui-table-cell>
                      <div class="flex items-center gap-3">
                        <ui-avatar class="size-8">
                          <ui-avatar-fallback class="text-xs font-medium">{{ initials(student.name) }}</ui-avatar-fallback>
                        </ui-avatar>
                        <div class="min-w-0">
                          <p class="truncate text-sm font-semibold">{{ student.name }}</p>
                          <p class="text-muted-foreground font-mono text-xs">{{ student.studentId }}</p>
                        </div>
                      </div>
                    </td>
                    <td ui-table-cell class="text-right font-medium tabular-nums">{{ student.homework }}</td>
                    <td ui-table-cell class="text-right font-medium tabular-nums">{{ student.midterm }}</td>
                    <td ui-table-cell class="text-right font-medium tabular-nums">{{ student.labProjects }}</td>
                    <td ui-table-cell class="text-right font-medium tabular-nums">{{ student.finalProject }}</td>
                    <td ui-table-cell class="text-foreground text-right font-bold tabular-nums">
                      {{ student.overallGrade }}
                    </td>
                    <td ui-table-cell class="text-center">
                      <span ui-badge variant="outline" [class]="gradeBadgeClass(student.letterGrade)">
                        {{ student.letterGrade }}
                      </span>
                    </td>
                    <td ui-table-cell>
                      <div class="inline-flex items-center gap-1.5 text-xs">
                        <span [class]="attendanceDotClass(student.absences)" aria-hidden="true"></span>
                        <span class="text-foreground font-medium tabular-nums">{{ student.attendanceRate }}</span>
                        <span class="text-muted-foreground">· {{ absenceLabel(student.absences) }}</span>
                      </div>
                    </td>
                    <td ui-table-cell class="text-right">
                      <ui-dropdown-menu>
                        <button
                          ui-button
                          ui-dropdown-menu-trigger
                          variant="ghost"
                          size="icon-sm"
                          class="text-muted-foreground"
                        >
                          <lucide-icon [img]="MoreHorizontal" class="size-4" />
                          <span class="sr-only">Open actions for {{ student.name }}</span>
                        </button>
                        <ui-dropdown-menu-content align="end" class="w-48">
                          <ui-dropdown-menu-item>
                            <lucide-icon [img]="Pencil" class="size-4" />
                            <span>Edit Grades</span>
                          </ui-dropdown-menu-item>
                          <ui-dropdown-menu-item>
                            <lucide-icon [img]="User" class="size-4" />
                            <span>View Student Profile</span>
                          </ui-dropdown-menu-item>
                          <ui-dropdown-menu-separator />
                          <ui-dropdown-menu-item>
                            <lucide-icon [img]="Mail" class="size-4" />
                            <span>Send Email Alert</span>
                          </ui-dropdown-menu-item>
                        </ui-dropdown-menu-content>
                      </ui-dropdown-menu>
                    </td>
                  </tr>
                }
              </tbody>
            </table>
          </div>
    
          <!-- Table Footer / Pagination -->
          <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
            <p class="text-muted-foreground text-sm">
              Showing <span class="text-foreground font-medium">6</span> of
              <span class="text-foreground font-medium">28</span> enrolled students
            </p>
            <div class="flex items-center gap-2">
              <button ui-button variant="outline" size="sm" disabled>Previous</button>
              <button ui-button variant="outline" size="sm">Next</button>
            </div>
          </div>
        </div>
      `,
    })
    export class UiStudentGradebookTableComponent {
      protected readonly Download = Download
      protected readonly Mail = Mail
      protected readonly MoreHorizontal = MoreHorizontal
      protected readonly Pencil = Pencil
      protected readonly Plus = Plus
      protected readonly User = User
    
      protected readonly metrics = metrics
      protected readonly students = students
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      metricIconClass(iconClass: string): string {
        return cn('flex size-8 items-center justify-center rounded-md', iconClass)
      }
    
      metricValueClass(valueClass: string): string {
        return cn('text-2xl font-bold tracking-tight tabular-nums', valueClass)
      }
    
      gradeBadgeClass(grade: LetterGrade): string {
        return cn('font-semibold', gradeBadgeConfig[grade].class)
      }
    
      attendanceDotClass(absences: number): string {
        return cn('inline-block size-1.5 rounded-full', absences === 0 ? 'bg-success' : 'bg-warning')
      }
    
      absenceLabel(absences: number): string {
        if (absences === 0) return '0 absences'
        return `${absences} absence${absences > 1 ? 's' : ''}`
      }
    
      initials(name: string): string {
        return initials(name)
      }
    }
    
  • src/app/components/blocks/student-gradebook-table/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/student-gradebook-table.json