Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
Binary file added src/.DS_Store
Binary file not shown.
Binary file added src/client/.DS_Store
Binary file not shown.
53 changes: 12 additions & 41 deletions src/client/certstack/.gitignore
Original file line number Diff line number Diff line change
@@ -1,41 +1,12 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
# v0 runtime files (should only exist in preview, not in git)
__v0_runtime_loader.js
__v0_devtools.tsx
__v0_jsx-dev-runtime.ts
instrumentation-client.js
instrumentation-client.ts

# Common ignores
node_modules/
.next/
.env*.local
.DS_Store
37 changes: 37 additions & 0 deletions src/client/certstack/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { AppLayout } from "@/components/app-layout"
import { AppHeader } from "@/components/app-header"
import { StatCards } from "@/components/dashboard/stat-cards"
import { ProgressChart } from "@/components/dashboard/progress-chart"
import { RecentActivity } from "@/components/dashboard/recent-activity"
import { SubjectProgress } from "@/components/dashboard/subject-progress"
import { UpcomingExams } from "@/components/dashboard/upcoming-exams"

export default function DashboardPage() {
return (
<AppLayout>
<AppHeader title="Dashboard" subtitle="Welcome back, John" />
<main className="flex-1 overflow-y-auto p-8">
<div className="mx-auto max-w-7xl">
{/* Stat Cards */}
<StatCards />

{/* Charts + Activity Row */}
<div className="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">
<ProgressChart />
</div>
<div>
<SubjectProgress />
</div>
</div>

{/* Activity + Upcoming Row */}
<div className="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
<RecentActivity />
<UpcomingExams />
</div>
</div>
</main>
</AppLayout>
)
}
305 changes: 305 additions & 0 deletions src/client/certstack/app/exams/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
"use client"

import { useState } from "react"
import { AppLayout } from "@/components/app-layout"
import { AppHeader } from "@/components/app-header"
import {
Search,
Filter,
Clock,
FileText,
CheckCircle,
Play,
ChevronDown,
Calendar,
} from "lucide-react"
import { cn } from "@/lib/utils"

type ExamStatus = "all" | "available" | "completed" | "upcoming"

interface Exam {
id: number
name: string
subject: string
questions: number
duration: string
difficulty: "Easy" | "Medium" | "Hard"
status: "available" | "completed" | "upcoming"
score?: number
date?: string
attempts?: number
}

const exams: Exam[] = [
{
id: 1,
name: "Anatomy Comprehensive Final",
subject: "Anatomy",
questions: 150,
duration: "3 hours",
difficulty: "Hard",
status: "available",
attempts: 0,
},
{
id: 2,
name: "Pharmacology Midterm Mock",
subject: "Pharmacology",
questions: 100,
duration: "2 hours",
difficulty: "Medium",
status: "completed",
score: 85,
date: "Jan 28, 2026",
attempts: 2,
},
{
id: 3,
name: "Biochemistry Unit 3 Test",
subject: "Biochemistry",
questions: 50,
duration: "1 hour",
difficulty: "Easy",
status: "completed",
score: 92,
date: "Jan 20, 2026",
attempts: 1,
},
{
id: 4,
name: "Pathology Practice Exam",
subject: "Pathology",
questions: 120,
duration: "2.5 hours",
difficulty: "Hard",
status: "available",
attempts: 0,
},
{
id: 5,
name: "Microbiology Final Review",
subject: "Microbiology",
questions: 80,
duration: "1.5 hours",
difficulty: "Medium",
status: "upcoming",
date: "Mar 5, 2026",
},
{
id: 6,
name: "Physiology Systems Exam",
subject: "Physiology",
questions: 100,
duration: "2 hours",
difficulty: "Medium",
status: "completed",
score: 73,
date: "Jan 15, 2026",
attempts: 1,
},
{
id: 7,
name: "Histology Lab Practical",
subject: "Histology",
questions: 60,
duration: "1 hour",
difficulty: "Easy",
status: "available",
attempts: 0,
},
{
id: 8,
name: "Genetics Comprehensive",
subject: "Genetics",
questions: 90,
duration: "2 hours",
difficulty: "Hard",
status: "upcoming",
date: "Mar 12, 2026",
},
]

const difficultyConfig = {
Easy: { bg: "#ECFDF5", text: "#047857" },
Medium: { bg: "#FEF3C7", text: "#B45309" },
Hard: { bg: "#FEE2E2", text: "#B91C1C" },
}

const statusConfig = {
available: { label: "Start Exam", icon: Play, color: "#4A7FFF" },
completed: { label: "Review", icon: CheckCircle, color: "#10B981" },
upcoming: { label: "Upcoming", icon: Calendar, color: "#F59E0B" },
}

const tabs: { label: string; value: ExamStatus }[] = [
{ label: "All Exams", value: "all" },
{ label: "Available", value: "available" },
{ label: "Completed", value: "completed" },
{ label: "Upcoming", value: "upcoming" },
]

export default function ExamsPage() {
const [activeTab, setActiveTab] = useState<ExamStatus>("all")
const [searchQuery, setSearchQuery] = useState("")

const filteredExams = exams.filter((exam) => {
const matchesTab = activeTab === "all" || exam.status === activeTab
const matchesSearch =
exam.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
exam.subject.toLowerCase().includes(searchQuery.toLowerCase())
return matchesTab && matchesSearch
})

return (
<AppLayout>
<AppHeader title="Exams" subtitle="Browse and take practice exams" />
<main className="flex-1 overflow-y-auto p-8">
<div className="mx-auto max-w-7xl">
{/* Filters Row */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
{/* Tabs */}
<div className="flex gap-1 rounded-xl bg-[hsl(var(--surface))] p-1">
{tabs.map((tab) => (
<button
key={tab.value}
onClick={() => setActiveTab(tab.value)}
className={cn(
"rounded-lg px-4 py-2 text-sm font-medium transition-all duration-150",
activeTab === tab.value
? "bg-[hsl(var(--surface-elevated))] text-[hsl(var(--text-primary))] shadow-sm"
: "text-[hsl(var(--text-secondary))] hover:text-[hsl(var(--text-primary))]"
)}
>
{tab.label}
</button>
))}
</div>

{/* Search + Filter */}
<div className="flex gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[hsl(var(--text-tertiary))]" />
<input
type="text"
placeholder="Search exams..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-10 w-64 rounded-xl border border-transparent bg-[hsl(var(--surface))] pl-10 pr-4 text-sm text-[hsl(var(--text-primary))] placeholder:text-[hsl(var(--text-tertiary))] transition-all duration-200 focus:border-[hsl(var(--border))] focus:bg-[hsl(var(--surface-elevated))] focus:outline-none focus:ring-2 focus:ring-[#DBEAFE]"
/>
</div>
<button className="flex h-10 items-center gap-2 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--surface-elevated))] px-4 text-sm font-medium text-[hsl(var(--text-secondary))] transition-colors duration-150 hover:bg-[hsl(var(--surface))]">
<Filter className="h-4 w-4" />
Filter
<ChevronDown className="h-3.5 w-3.5" />
</button>
</div>
</div>

{/* Exam Table */}
<div className="overflow-hidden rounded-2xl border border-[hsl(var(--border-light))] bg-[hsl(var(--surface-elevated))] shadow-[0_1px_3px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)]">
{/* Table Header */}
<div className="grid grid-cols-[2fr_1fr_1fr_1fr_1fr_120px] gap-4 border-b border-[hsl(var(--border))] bg-[hsl(var(--surface))] px-6 py-3">
<span className="text-xs font-semibold uppercase tracking-wider text-[hsl(var(--text-secondary))]">
Exam Name
</span>
<span className="text-xs font-semibold uppercase tracking-wider text-[hsl(var(--text-secondary))]">
Subject
</span>
<span className="text-xs font-semibold uppercase tracking-wider text-[hsl(var(--text-secondary))]">
Questions
</span>
<span className="text-xs font-semibold uppercase tracking-wider text-[hsl(var(--text-secondary))]">
Duration
</span>
<span className="text-xs font-semibold uppercase tracking-wider text-[hsl(var(--text-secondary))]">
Difficulty
</span>
<span className="text-xs font-semibold uppercase tracking-wider text-[hsl(var(--text-secondary))]">
Action
</span>
</div>

{/* Table Rows */}
{filteredExams.map((exam) => {
const difficulty = difficultyConfig[exam.difficulty]
const status = statusConfig[exam.status]
const StatusIcon = status.icon
return (
<div
key={exam.id}
className="grid grid-cols-[2fr_1fr_1fr_1fr_1fr_120px] items-center gap-4 border-b border-[hsl(var(--border-light))] px-6 py-4 transition-colors duration-150 last:border-b-0 hover:bg-[hsl(var(--surface))]"
>
{/* Name */}
<div>
<p className="text-sm font-medium text-[hsl(var(--text-primary))]">{exam.name}</p>
{exam.status === "completed" && exam.score !== undefined && (
<p className="mt-0.5 text-xs text-[hsl(var(--text-tertiary))]">
Score: {exam.score}% | {exam.attempts} attempt{exam.attempts !== 1 ? "s" : ""}
</p>
)}
{exam.status === "upcoming" && exam.date && (
<p className="mt-0.5 text-xs text-[hsl(var(--text-tertiary))]">
Available: {exam.date}
</p>
)}
</div>

{/* Subject */}
<span className="text-sm text-[hsl(var(--text-secondary))]">{exam.subject}</span>

{/* Questions */}
<div className="flex items-center gap-1.5 text-sm text-[hsl(var(--text-secondary))]">
<FileText className="h-3.5 w-3.5" />
{exam.questions}
</div>

{/* Duration */}
<div className="flex items-center gap-1.5 text-sm text-[hsl(var(--text-secondary))]">
<Clock className="h-3.5 w-3.5" />
{exam.duration}
</div>

{/* Difficulty */}
<span
className="inline-flex h-6 w-fit items-center rounded-full px-3 text-xs font-medium"
style={{ backgroundColor: difficulty.bg, color: difficulty.text }}
>
{exam.difficulty}
</span>

{/* Action */}
<button
disabled={exam.status === "upcoming"}
className={cn(
"flex h-9 items-center justify-center gap-1.5 rounded-full text-xs font-medium transition-all duration-200",
exam.status === "upcoming"
? "cursor-not-allowed bg-[hsl(var(--surface))] text-[hsl(var(--text-tertiary))]"
: exam.status === "completed"
? "bg-[#ECFDF5] text-[#047857] hover:bg-[#D1FAE5]"
: "bg-[#4A7FFF] text-[#FFFFFF] hover:bg-[#3D6EE8]"
)}
>
<StatusIcon className="h-3.5 w-3.5" />
{status.label}
</button>
</div>
)
})}

{filteredExams.length === 0 && (
<div className="flex flex-col items-center justify-center py-16">
<FileText className="h-12 w-12 text-[hsl(var(--text-tertiary))]" />
<p className="mt-4 text-sm font-medium text-[hsl(var(--text-secondary))]">
No exams found
</p>
<p className="mt-1 text-xs text-[hsl(var(--text-tertiary))]">
Try adjusting your search or filter
</p>
</div>
)}
</div>
</div>
</main>
</AppLayout>
)
}
Binary file removed src/client/certstack/app/favicon.ico
Binary file not shown.
Loading