Menu

Change language

GitHub repo

Course Curriculum Outline

blockeducation

LMS and video course syllabus block: hero header with course metadata, instructor profile, rating, progress bar, and action buttons, collapsible modular section accordions with lesson durations, type badges, and completion checks, plus a sidebar with verifiable certificate preview, downloadable course resources, and student community access.

Installation

$npx uipkge-ng@latest add course-curriculum-outline
Manifest:https://uipkge.dev/r/angular/course-curriculum-outline.jsonInstalls to:src/app/components/blocks/course-curriculum-outline/

Variants

Loading interactive previews…

Schema

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

Lesson
interface Lesson {
  id: string
  title: string
  duration: string
  type: 'Video' | 'Interactive Lab' | 'Quiz'
  status: 'completed' | 'in-progress' | 'not-started' | 'locked'
}
Module
interface Module {
  id: string
  title: string
  duration: string
  lessonsCount: string
  completedCount: number
  totalCount: number
  status: 'completed' | 'in-progress' | 'locked'
  lessons: Lesson[]
}
CourseResource
interface CourseResource {
  id: string
  title: string
  meta: string
  type: 'figma' | 'repo' | 'pdf'
}

Files installed (2)

  • src/app/components/blocks/course-curriculum-outline/course-curriculum-outline.component.ts29.9 kB
    // LMS video-course syllabus: hero header with course metadata, instructor profile,
    // rating, progress bar and actions, collapsible module accordions with lesson
    // durations, type badges and completion checks, plus a sidebar with a verifiable
    // certificate preview, downloadable resources and community access.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Award,
      Check,
      CheckCircle2,
      Circle,
      Clock,
      Download,
      ExternalLink,
      FileCode,
      FileQuestion,
      FileText,
      FlaskConical,
      GraduationCap,
      Lock,
      LucideAngularModule,
      MessageSquare,
      PenTool,
      Play,
      PlayCircle,
      ShieldCheck,
      Star,
      Users,
      Video,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import {
      UiAccordionComponent,
      UiAccordionContentComponent,
      UiAccordionItemComponent,
      UiAccordionTriggerComponent,
    } from '@/ui/accordion/accordion.component'
    import { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component'
    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'
    
    export interface Lesson {
      id: string
      title: string
      duration: string
      type: 'Video' | 'Interactive Lab' | 'Quiz'
      status: 'completed' | 'in-progress' | 'not-started' | 'locked'
    }
    
    export interface Module {
      id: string
      title: string
      duration: string
      lessonsCount: string
      completedCount: number
      totalCount: number
      status: 'completed' | 'in-progress' | 'locked'
      lessons: Lesson[]
    }
    
    interface CourseResource {
      id: string
      title: string
      meta: string
      type: 'figma' | 'repo' | 'pdf'
    }
    
    export const defaultModules: Module[] = [
      {
        id: 'module-1',
        title: 'Module 1: Design Tokens & OKLCH Architecture',
        duration: '1h 45m',
        lessonsCount: '4 lessons',
        completedCount: 4,
        totalCount: 4,
        status: 'completed',
        lessons: [
          {
            id: 'm1-l1',
            title: 'Introduction to OKLCH & Gamut Mapping',
            duration: '18m',
            type: 'Video',
            status: 'completed',
          },
          {
            id: 'm1-l2',
            title: 'Configuring Semantic Tokens in Tailwind CSS v4',
            duration: '26m',
            type: 'Video',
            status: 'completed',
          },
          {
            id: 'm1-l3',
            title: 'Lab: Building an Automated Contrast Checker',
            duration: '35m',
            type: 'Interactive Lab',
            status: 'completed',
          },
          {
            id: 'm1-l4',
            title: 'Quiz: Token Architecture & Dark Mode Strategy',
            duration: '26m',
            type: 'Quiz',
            status: 'completed',
          },
        ],
      },
      {
        id: 'module-2',
        title: 'Module 2: Polymorphic Primitives & Headless Reka UI',
        duration: '3h 10m',
        lessonsCount: '6 lessons',
        completedCount: 3,
        totalCount: 6,
        status: 'in-progress',
        lessons: [
          {
            id: 'm2-l1',
            title: 'Anatomy of Headless Primitives with Reka UI & Radix',
            duration: '24m',
            type: 'Video',
            status: 'completed',
          },
          {
            id: 'm2-l2',
            title: 'Building Polymorphic Button & Slot Mechanics',
            duration: '32m',
            type: 'Video',
            status: 'completed',
          },
          {
            id: 'm2-l3',
            title: 'Lab: Compound Component Patterns with Provide/Inject',
            duration: '40m',
            type: 'Interactive Lab',
            status: 'completed',
          },
          {
            id: 'm2-l4',
            title: 'Accessible Dialog, Popover & Focus Trapping',
            duration: '36m',
            type: 'Video',
            status: 'in-progress',
          },
          {
            id: 'm2-l5',
            title: 'Virtualization for High-Density Data Grids',
            duration: '38m',
            type: 'Video',
            status: 'not-started',
          },
          {
            id: 'm2-l6',
            title: 'Quiz: Accessibility Tree & ARIA Roles',
            duration: '20m',
            type: 'Quiz',
            status: 'not-started',
          },
        ],
      },
      {
        id: 'module-3',
        title: 'Module 3: Micro-Interactions & Spring Physics',
        duration: '4h 50m',
        lessonsCount: '8 lessons',
        completedCount: 0,
        totalCount: 8,
        status: 'locked',
        lessons: [
          {
            id: 'm3-l1',
            title: 'Physics-Based Animation Principles with Spring Physics',
            duration: '28m',
            type: 'Video',
            status: 'locked',
          },
          {
            id: 'm3-l2',
            title: 'Gesture Interactions & Drag-and-Drop Primitives',
            duration: '35m',
            type: 'Interactive Lab',
            status: 'locked',
          },
          {
            id: 'm3-l3',
            title: 'Layout Animations and Shared Element Transitions',
            duration: '42m',
            type: 'Video',
            status: 'locked',
          },
          {
            id: 'm3-l4',
            title: 'Haptic Feedback & Tactile States in Web Interfaces',
            duration: '25m',
            type: 'Video',
            status: 'locked',
          },
          {
            id: 'm3-l5',
            title: 'Optimizing 60fps / 120fps Rendering Pipelines',
            duration: '45m',
            type: 'Video',
            status: 'locked',
          },
          {
            id: 'm3-l6',
            title: 'Lab: Crafting a Fluid Floating Command Palette',
            duration: '50m',
            type: 'Interactive Lab',
            status: 'locked',
          },
          { id: 'm3-l7', title: 'Sound Design & Audio Micro-Feedback', duration: '25m', type: 'Video', status: 'locked' },
          {
            id: 'm3-l8',
            title: 'Quiz: Motion Reduction & Animation Performance',
            duration: '20m',
            type: 'Quiz',
            status: 'locked',
          },
        ],
      },
      {
        id: 'module-4',
        title: 'Module 4: Monorepo Architecture & CI Distribution',
        duration: '4h 45m',
        lessonsCount: '6 lessons',
        completedCount: 0,
        totalCount: 6,
        status: 'locked',
        lessons: [
          {
            id: 'm4-l1',
            title: 'Turborepo & pnpm Workspaces Setup from Scratch',
            duration: '35m',
            type: 'Video',
            status: 'locked',
          },
          {
            id: 'm4-l2',
            title: 'Dual-Framework Export Pipelines (Vue 3 + React 19)',
            duration: '45m',
            type: 'Video',
            status: 'locked',
          },
          {
            id: 'm4-l3',
            title: 'Registry Distribution Schema & JSON Manifest Generator',
            duration: '50m',
            type: 'Video',
            status: 'locked',
          },
          {
            id: 'm4-l4',
            title: 'Lab: Custom CLI Tooling with npx shadcn add',
            duration: '55m',
            type: 'Interactive Lab',
            status: 'locked',
          },
          {
            id: 'm4-l5',
            title: 'Automated Visual Regression CI with Playwright',
            duration: '40m',
            type: 'Interactive Lab',
            status: 'locked',
          },
          {
            id: 'm4-l6',
            title: 'Capstone Project: Publishing Your Own Component Registry',
            duration: '60m',
            type: 'Interactive Lab',
            status: 'locked',
          },
        ],
      },
    ]
    
    const resources: CourseResource[] = [
      { id: 'res-figma', title: 'Figma Design System Kit v4.2', meta: '.fig · 48.2 MB', type: 'figma' },
      { id: 'res-repo', title: 'Course Starter Monorepo Template', meta: 'GitHub Template · Public', type: 'repo' },
      { id: 'res-tokens', title: 'OKLCH & Color Tokens Cheatsheet', meta: 'PDF Guide · 2.4 MB', type: 'pdf' },
      { id: 'res-wcag', title: 'WCAG 2.2 AA Contrast Matrix', meta: 'PDF Reference · 1.8 MB', type: 'pdf' },
    ]
    
    export const totalLessonsCount = 24
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-course-curriculum-outline, [ui-course-curriculum-outline]',
      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,
        UiAccordionComponent,
        UiAccordionContentComponent,
        UiAccordionItemComponent,
        UiAccordionTriggerComponent,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiAvatarImageComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardFooterComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiProgressComponent,
      ],
      template: `
        <div data-slot="course-curriculum-outline" [class]="rootClass">
          <!-- Course Hero Header -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-header class="space-y-4">
              <div class="flex flex-wrap items-center justify-between gap-3">
                <div class="flex flex-wrap items-center gap-2">
                  <span ui-badge variant="secondary" class="font-medium">Advanced · 14.5 Total Hours</span>
                  <span ui-badge variant="outline" class="text-muted-foreground">Design Engineering</span>
                </div>
                <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <div class="text-warning flex items-center">
                    <lucide-icon [img]="Star" class="size-3.5 fill-current" />
                    <span class="text-foreground ml-1 font-semibold">4.9</span>
                  </div>
                  <span>·</span>
                  <span class="tabular-nums">(1,840 students)</span>
                </div>
              </div>
    
              <div class="space-y-2">
                <h2 ui-card-title class="text-2xl font-bold tracking-tight sm:text-3xl">
                  Full-Stack Design Systems with Vue & React
                </h2>
                <p ui-card-description class="text-sm">
                  Master enterprise-grade UI components, OKLCH color spaces, headless architecture with Reka UI, fluid
                  spring physics, and monorepo package distribution.
                </p>
              </div>
    
              <div class="flex flex-wrap items-center justify-between gap-4 pt-1">
                <div class="flex items-center gap-3">
                  <ui-avatar class="border-border size-10 border">
                    <ui-avatar-image
                      src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=96&h=96&fit=crop&crop=faces"
                      alt="Marcus Vance"
                    />
                    <ui-avatar-fallback>MV</ui-avatar-fallback>
                  </ui-avatar>
                  <div>
                    <p class="text-sm leading-none font-medium">Marcus Vance</p>
                    <p class="text-muted-foreground mt-1 text-xs">Principal Design Engineer</p>
                  </div>
                </div>
    
                <div class="flex flex-wrap items-center gap-2.5">
                  @if (!isFullyCompleted()) {
                    <button ui-button class="gap-2 shadow-xs">
                      <lucide-icon [img]="Play" class="size-4 fill-current" />
                      Resume Learning
                    </button>
                  } @else {
                    <button ui-button aria-label="Download attachment" class="gap-2 shadow-xs">
                      <lucide-icon [img]="Award" class="size-4" />
                      Download Certificate
                    </button>
                  }
                  <button ui-button aria-label="Download attachment" variant="outline" class="gap-2">
                    <lucide-icon [img]="Download" class="size-4" />
                    Download Resources
                  </button>
                </div>
              </div>
            </div>
    
            <div ui-card-content class="pt-0">
              <div class="border-border/60 bg-muted/40 space-y-2 rounded-lg border p-4">
                <div class="flex items-center justify-between text-xs">
                  <span class="text-foreground font-medium">
                    <span class="tabular-nums">{{ completedLessonsCount() }} of {{ totalLessonsCount }}</span> lessons
                    completed
                  </span>
                  <span class="text-muted-foreground font-medium tabular-nums">{{ progressPercent() }}%</span>
                </div>
                <div ui-progress [value]="progressPercent()"></div>
              </div>
            </div>
          </div>
    
          <!-- Main Content & Sidebar Grid -->
          <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
            <!-- Left Column: Modular Curriculum Accordions -->
            <div class="space-y-4 lg:col-span-2">
              <div class="flex items-center justify-between px-1">
                <div>
                  <h2 class="text-lg font-semibold tracking-tight">Course Curriculum</h2>
                  <p class="text-muted-foreground text-xs">4 modular sections · 24 total lessons & interactive labs</p>
                </div>
                <div class="text-muted-foreground text-xs tabular-nums">
                  <span class="text-foreground font-medium">{{ completedLessonsCount() }}/{{ totalLessonsCount }}</span>
                  Completed
                </div>
              </div>
    
              <ui-accordion type="multiple" [defaultValue]="openModules" variant="separated">
                @for (module of modules(); track module.id) {
                  <ui-accordion-item [value]="module.id">
                    <button ui-accordion-trigger class="hover:no-underline">
                      <div class="flex flex-1 items-center justify-between gap-3 pr-2 text-left">
                        <div class="flex min-w-0 items-center gap-3">
                          @if (module.status === 'completed') {
                            <span
                              class="border-success/30 bg-success/15 text-success flex size-7 shrink-0 items-center justify-center rounded-full border shadow-xs"
                            >
                              <lucide-icon [img]="Check" class="size-3.5" />
                            </span>
                          } @else if (module.status === 'in-progress') {
                            <span
                              class="border-primary/20 bg-primary/10 text-primary flex size-7 shrink-0 items-center justify-center rounded-full border"
                            >
                              <lucide-icon [img]="Play" class="size-3.5 fill-current" />
                            </span>
                          } @else {
                            <span
                              class="border-border bg-muted text-muted-foreground flex size-7 shrink-0 items-center justify-center rounded-full border"
                            >
                              <lucide-icon [img]="Lock" class="size-3.5" />
                            </span>
                          }
                          <div class="min-w-0">
                            <p class="text-foreground truncate text-sm font-semibold">{{ module.title }}</p>
                            <p class="text-muted-foreground mt-0.5 text-xs tabular-nums">
                              {{ module.lessonsCount }} · {{ module.duration }} ·
                              @if (module.status === 'completed') {
                                <span class="text-success font-medium">100% completed</span>
                              } @else if (module.status === 'in-progress') {
                                <span class="text-foreground font-medium">
                                  {{ module.completedCount }}/{{ module.totalCount }} completed
                                </span>
                              } @else {
                                <span class="text-muted-foreground">Locked</span>
                              }
                            </p>
                          </div>
                        </div>
    
                        <div class="hidden shrink-0 items-center gap-2 sm:flex">
                          @if (module.status === 'completed') {
                            <span ui-badge variant="success" class="gap-1 text-xs">
                              <lucide-icon [img]="Check" class="size-3" /> Completed
                            </span>
                          } @else if (module.status === 'in-progress') {
                            <span ui-badge variant="secondary" class="text-xs">
                              In Progress ({{ module.completedCount }}/{{ module.totalCount }})
                            </span>
                          } @else {
                            <span ui-badge variant="outline" class="text-muted-foreground gap-1 text-xs">
                              <lucide-icon [img]="Lock" class="size-3" /> Locked
                            </span>
                          }
                        </div>
                      </div>
                    </button>
    
                    <ui-accordion-content>
                      <div class="border-border divide-border divide-y border-t">
                        @for (lesson of module.lessons; track lesson.id) {
                          <div
                            class="group hover:bg-muted/40 flex items-center justify-between gap-3 px-2 py-3 transition-colors first:pt-3 last:pb-1 sm:px-3"
                          >
                            <div class="flex min-w-0 items-center gap-3">
                              <div class="flex shrink-0 items-center justify-center">
                                @if (lesson.status === 'completed') {
                                  <lucide-icon [img]="CheckCircle2" class="text-success size-4" />
                                } @else if (lesson.status === 'in-progress') {
                                  <lucide-icon [img]="PlayCircle" class="text-primary size-4 animate-pulse" />
                                } @else if (lesson.status === 'locked') {
                                  <lucide-icon [img]="Lock" class="text-muted-foreground/50 size-4" />
                                } @else {
                                  <lucide-icon [img]="Circle" class="text-muted-foreground/60 size-4" />
                                }
                              </div>
    
                              <div class="min-w-0">
                                <p [class]="lessonTitleClass(lesson)">{{ lesson.title }}</p>
                              </div>
                            </div>
    
                            <div class="flex shrink-0 items-center gap-2">
                              @if (lesson.type === 'Interactive Lab') {
                                <span ui-badge variant="secondary" class="gap-1 text-xs">
                                  <lucide-icon [img]="FlaskConical" class="size-3" />
                                  <span class="hidden sm:inline">Interactive Lab</span>
                                  <span class="sm:hidden">Lab</span>
                                </span>
                              } @else if (lesson.type === 'Quiz') {
                                <span ui-badge variant="outline" class="gap-1 text-xs">
                                  <lucide-icon [img]="FileQuestion" class="size-3" />
                                  Quiz
                                </span>
                              } @else {
                                <span ui-badge variant="outline" class="gap-1 text-xs">
                                  <lucide-icon [img]="Video" class="size-3" />
                                  Video
                                </span>
                              }
    
                              <span class="text-muted-foreground min-w-[32px] text-right font-mono text-xs tabular-nums">
                                {{ lesson.duration }}
                              </span>
    
                              <div class="hidden sm:block">
                                @if (lesson.status === 'in-progress') {
                                  <button ui-button size="sm" class="h-7 gap-1 px-2.5 text-xs">
                                    <lucide-icon [img]="Play" class="size-3 fill-current" /> Resume
                                  </button>
                                } @else if (lesson.status === 'completed') {
                                  <button
                                    ui-button
                                    variant="ghost"
                                    size="sm"
                                    class="text-muted-foreground hover:text-foreground h-7 px-2 text-xs"
                                  >
                                    Review
                                  </button>
                                } @else if (lesson.status === 'not-started') {
                                  <button ui-button variant="outline" size="sm" class="h-7 px-2 text-xs">Start</button>
                                } @else {
                                  <span class="text-muted-foreground/50 px-2 text-xs">Locked</span>
                                }
                              </div>
                            </div>
                          </div>
                        }
                      </div>
                    </ui-accordion-content>
                  </ui-accordion-item>
                }
              </ui-accordion>
            </div>
    
            <!-- Right Column: Certificate & Resources Sidebar -->
            <div class="space-y-6">
              <!-- Certificate Card -->
              <div ui-card>
                <div ui-card-header class="pb-3">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="GraduationCap" class="text-primary size-5" />
                    <h3 ui-card-title class="text-base">Certificate of Completion</h3>
                  </div>
                  <p ui-card-description class="text-xs">Accredited credential upon 100% course and lab completion.</p>
                </div>
    
                <div ui-card-content class="space-y-4">
                  <!-- Cert preview container -->
                  <div
                    class="border-border/80 from-muted/50 to-muted/20 relative space-y-3 overflow-hidden rounded-lg border bg-gradient-to-b p-4 text-center"
                  >
                    <div class="relative z-10 space-y-2">
                      <div [class]="certBadgeClass">
                        <lucide-icon [img]="Award" class="size-6" />
                      </div>
                      <div>
                        <p class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                          Verified Credential
                        </p>
                        <p class="text-foreground mt-0.5 text-sm font-bold">Full-Stack Design Systems</p>
                        <p class="text-muted-foreground text-xs">Marcus Vance · UIPKGE Academy</p>
                      </div>
                      <div class="pt-1">
                        <span
                          class="border-border bg-background/80 text-muted-foreground inline-flex items-center gap-1 rounded border px-2 py-0.5 font-mono text-xs"
                        >
                          <lucide-icon [img]="ShieldCheck" class="text-primary size-3" /> CERT-8841-FS
                        </span>
                      </div>
                    </div>
                  </div>
    
                  <!-- Progress info -->
                  <div class="space-y-1.5">
                    <div class="flex items-center justify-between text-xs">
                      <span class="text-muted-foreground">Graduation Status</span>
                      <span class="text-foreground font-medium tabular-nums">
                        {{ completedLessonsCount() }}/{{ totalLessonsCount }} Lessons
                      </span>
                    </div>
                    <div ui-progress [value]="progressPercent()"></div>
                    <p class="text-muted-foreground pt-1 text-xs">
                      @if (isFullyCompleted()) {
                        <span class="text-success font-medium">
                          All requirements satisfied! Your certificate is ready to download and share.
                        </span>
                      } @else {
                        <span>
                          Complete the remaining {{ totalLessonsCount - completedLessonsCount() }} lessons and labs to
                          unlock.
                        </span>
                      }
                    </p>
                  </div>
                </div>
    
                <div ui-card-footer>
                  @if (isFullyCompleted()) {
                    <button ui-button aria-label="Download attachment" class="w-full gap-1.5" size="sm">
                      <lucide-icon [img]="Download" class="size-4" /> Download PDF Certificate
                    </button>
                  } @else {
                    <button ui-button variant="outline" size="sm" class="w-full gap-1.5" disabled>
                      <lucide-icon [img]="Lock" class="size-4" /> Locked (Complete Course)
                    </button>
                  }
                </div>
              </div>
    
              <!-- Course Resources Card -->
              <div ui-card>
                <div ui-card-header class="pb-3">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="FileCode" class="text-primary size-5" />
                    <h3 ui-card-title class="text-base">Course Resources</h3>
                  </div>
                  <p ui-card-description class="text-xs">Downloadable starter kits, Figma files, and spec guides.</p>
                </div>
    
                <div ui-card-content class="space-y-2.5">
                  @for (res of courseResources; track res.id) {
                    <div
                      class="border-border hover:bg-muted/40 flex items-center justify-between gap-3 rounded-md border p-2.5 transition-colors"
                    >
                      <div class="flex min-w-0 items-center gap-2.5">
                        <span
                          class="bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-md"
                        >
                          @if (res.type === 'repo') {
                            <lucide-icon [img]="FileCode" class="size-4" />
                          } @else if (res.type === 'figma') {
                            <lucide-icon [img]="PenTool" class="size-4" />
                          } @else {
                            <lucide-icon [img]="FileText" class="size-4" />
                          }
                        </span>
                        <div class="min-w-0">
                          <p class="text-foreground truncate text-xs font-medium">{{ res.title }}</p>
                          <p class="text-muted-foreground text-xs">{{ res.meta }}</p>
                        </div>
                      </div>
    
                      <button
                        ui-button
                        variant="ghost"
                        size="icon"
                        class="text-muted-foreground hover:text-foreground size-8 shrink-0"
                      >
                        @if (res.type === 'repo') {
                          <lucide-icon [img]="ExternalLink" class="size-4" />
                        } @else {
                          <lucide-icon [img]="Download" class="size-4" />
                        }
                        <span class="sr-only">Download {{ res.title }}</span>
                      </button>
                    </div>
                  }
                </div>
              </div>
    
              <!-- Community & Mentorship Card -->
              <div ui-card>
                <div ui-card-header class="pb-3">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="Users" class="text-primary size-5" />
                    <h3 ui-card-title class="text-base">Student Community</h3>
                  </div>
                  <p ui-card-description class="text-xs">
                    Connect with peers and get code reviews from design engineering mentors.
                  </p>
                </div>
    
                <div ui-card-content class="text-muted-foreground space-y-2.5 text-xs">
                  <div class="flex items-center gap-2">
                    <span class="bg-success size-2 rounded-full"></span>
                    <span><strong class="text-foreground">1,420 students online</strong> in Discord</span>
                  </div>
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="Clock" class="text-muted-foreground size-3.5" />
                    <span>Weekly live Q&A: <strong class="text-foreground">Thursdays @ 5:00 PM UTC</strong></span>
                  </div>
                </div>
    
                <div ui-card-footer>
                  <button ui-button variant="outline" size="sm" class="w-full gap-1.5">
                    <lucide-icon [img]="MessageSquare" class="size-4" /> Join Discord Community
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiCourseCurriculumOutlineComponent {
      protected readonly Award = Award
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Circle = Circle
      protected readonly Clock = Clock
      protected readonly Download = Download
      protected readonly ExternalLink = ExternalLink
      protected readonly FileCode = FileCode
      protected readonly FileQuestion = FileQuestion
      protected readonly FileText = FileText
      protected readonly FlaskConical = FlaskConical
      protected readonly GraduationCap = GraduationCap
      protected readonly Lock = Lock
      protected readonly MessageSquare = MessageSquare
      protected readonly PenTool = PenTool
      protected readonly Play = Play
      protected readonly PlayCircle = PlayCircle
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Star = Star
      protected readonly Users = Users
      protected readonly Video = Video
    
      protected readonly openModules = ['module-1', 'module-2']
      protected readonly courseResources = resources
      protected readonly totalLessonsCount = totalLessonsCount
    
      /** Seed completed state for all lessons to preview the verified certificate state. */
      @Input() set initialComplete(v: boolean) {
        this.initialCompleteSig.set(!!v)
      }
      @Input('class') className?: string
    
      private readonly initialCompleteSig = signal(false)
    
      readonly modules = computed<Module[]>(() => {
        if (!this.initialCompleteSig()) return defaultModules
        return defaultModules.map((m) => ({
          ...m,
          status: 'completed' as const,
          completedCount: m.totalCount,
          lessons: m.lessons.map((l) => ({ ...l, status: 'completed' as const })),
        }))
      })
    
      readonly completedLessonsCount = computed(() => (this.initialCompleteSig() ? 24 : 8))
      readonly progressPercent = computed(() => Math.round((this.completedLessonsCount() / totalLessonsCount) * 100))
      readonly isFullyCompleted = computed(() => this.completedLessonsCount() === totalLessonsCount)
    
      get rootClass(): string {
        return cn('text-foreground w-full space-y-6', this.className)
      }
    
      get certBadgeClass(): string {
        return cn(
          'mx-auto flex size-12 items-center justify-center rounded-full border shadow-xs',
          this.isFullyCompleted()
            ? 'border-success/30 bg-success/15 text-success'
            : 'border-primary/30 bg-primary/10 text-primary',
        )
      }
    
      lessonTitleClass(lesson: Lesson): string {
        return cn(
          'truncate text-sm font-medium transition-colors',
          lesson.status === 'locked' ? 'text-muted-foreground' : 'text-foreground group-hover:text-primary',
        )
      }
    }
    
  • src/app/components/blocks/course-curriculum-outline/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/course-curriculum-outline.json