Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
aa8d69c
Add in-person judge portal at /judging with critique-driven UX fixes.
alysibak Jul 4, 2026
a037c1c
feat(admin): stats dashboard + schedule manager against real submissions
t9nzin Jul 5, 2026
04fdaa4
Add judge identity via organizer URL code param.
alysibak Jul 5, 2026
b6c29e5
Use judgments table as authoritative sync store with deterministic keys.
alysibak Jul 5, 2026
a8eecd4
Split judged and skipped counts in reporting and add skip reasons.
alysibak Jul 5, 2026
65f3a52
Decouple debounced notes sync and apply server clock offset at load.
alysibak Jul 5, 2026
76aa760
Add on-brand cold-start retry screen when judging data fetch fails.
alysibak Jul 5, 2026
98cf83f
Disable mark and skip CTAs under production mock banner.
alysibak Jul 5, 2026
3869174
Polish judging UX: inline reset confirm, early mark persistence, slot…
alysibak Jul 5, 2026
b898612
Add Vitest coverage for judging format and slots time logic.
alysibak Jul 5, 2026
4ec095a
Complete judging portal infrastructure and update README.
alysibak Jul 5, 2026
d45bec4
added things and fixed ui
alysibak Jul 6, 2026
cdb1c47
add: general layout of volunteer dashboard, db string, mobile respons…
ahha220 Jul 6, 2026
edc70c7
add: sponsor page
ahha220 Jul 6, 2026
e5fd54f
feat(admin): schedule grid + hacker submissions CRUD
t9nzin Jul 13, 2026
cb93933
feat(admin): persist schedule to DB with full CRUD
t9nzin Jul 13, 2026
2ce9776
fix ui
alysibak Jul 13, 2026
023e4fe
add: db sync schema + update events page
ahha220 Jul 13, 2026
c78266c
fix: calendar view
ahha220 Jul 13, 2026
9698116
fix: alignment changes on the calendar
ahha220 Jul 13, 2026
4b1f0fc
updated package
faizm10 Jul 14, 2026
95c1328
Merge pull request #26 from Hack-Canada/amy/volunteer-sponsor
faizm10 Jul 14, 2026
32d1dee
Remove unused scripts and database schema files; delete seed script f…
faizm10 Jul 14, 2026
5fab056
feat(admin): add legend
t9nzin Jul 16, 2026
74d3ff7
Merge pull request #30 from Hack-Canada/tenzin/admin
faizm10 Jul 16, 2026
cde36df
merging changes from development
Krixsick Jul 16, 2026
29d752e
Merge pull request #29 from Hack-Canada/linus/hacker
Krixsick Jul 16, 2026
39fea1f
Merge branch 'development' into aly/judging
alysibak Jul 16, 2026
48666bb
Merge branch 'development' into aly/judging
alysibak Jul 16, 2026
24c6368
Merge pull request #32 from Hack-Canada/aly-judging
faizm10 Jul 16, 2026
cab57c3
Merge remote-tracking branch 'origin/main' into merge-main-into-devel…
faizm10 Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.cursor/
5 changes: 2 additions & 3 deletions frontend/.gitignore β†’ client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

# dependencies
/node_modules
package-lock.json
/.pnp
.pnp.*
.yarn/*
Expand Down Expand Up @@ -33,6 +32,7 @@ yarn-error.log*

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

# vercel
.vercel
Expand All @@ -41,5 +41,4 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts

#env
.env.local
/scripts
5 changes: 5 additions & 0 deletions client/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes β€” APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions client/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
35 changes: 1 addition & 34 deletions frontend/README.md β†’ client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Fredoka](https://fonts.google.com/specimen/Fredoka) and [Rubik](https://fonts.google.com/specimen/Rubik) fonts from Google Fonts.

## Pre-commit hooks

The repo uses [pre-commit](https://pre-commit.com/) to run checks before each commit. The config lives in the **project root** (`.pre-commit-config.yaml`) and currently runs [Gitleaks](https://github.com/gitleaks/gitleaks) to scan for secrets.

**If you want to use it:**

1. Install the pre-commit dependency (from the **repo root**, not the frontend folder):

```bash
cd .. # if you're in frontend/
pip install -r requirements-dev.txt
```

Or install pre-commit directly:

```bash
pip install pre-commit
```

2. Install the git hooks (from the repo root):

```bash
pre-commit install
```

3. After that, every `git commit` will run the hooks automatically. To run them once without committing:

```bash
pre-commit run --all-files
```

The first run may take a minute while pre-commit downloads the Gitleaks environment; later runs are fast.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

Expand Down
28 changes: 28 additions & 0 deletions client/app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AdminNav } from "@/components/admin/admin-nav";
import { Toaster } from "@/components/ui/sonner";

export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-full flex-1 flex-col bg-background text-foreground">
<header className="border-b bg-card">
<div className="mx-auto w-full max-w-6xl px-6 py-4">
<div className="mb-3 flex items-baseline gap-3">
<h1 className="text-lg font-semibold tracking-tight">Admin</h1>
<span className="text-sm text-muted-foreground">
HackCanada Judging Platform
</span>
</div>
<AdminNav />
</div>
</header>
<main className="mx-auto w-full max-w-6xl flex-1 px-6 py-8">
{children}
</main>
<Toaster />
</div>
);
}
5 changes: 5 additions & 0 deletions client/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";

export default function AdminPage() {
redirect("/admin/stats");
}
38 changes: 38 additions & 0 deletions client/app/admin/schedule/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getProjects } from "@/lib/queries";
import {
seedScheduleIfEmpty,
getScheduleSlots,
DEFAULT_ROOMS,
} from "@/lib/schedule";
import { ScheduleManager } from "@/components/admin/schedule-manager";

export const metadata = { title: "Admin Β· Schedule" };

export default async function SchedulePage() {
const projects = await getProjects();
// Seed the schedule from the derived draft on first visit, then read it back.
await seedScheduleIfEmpty(projects);
const slots = await getScheduleSlots();

return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold tracking-tight">
Schedule manager
</h2>
<p className="text-sm text-muted-foreground">
Override pitch times, add delays, and search across all projects.
</p>
</div>
<ScheduleManager
initialSlots={slots}
rooms={DEFAULT_ROOMS}
projects={projects.map((p) => ({
id: p.id,
name: p.project_name,
track: p.tracks[0] ?? "General",
}))}
/>
</div>
);
}
133 changes: 133 additions & 0 deletions client/app/admin/stats/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { FolderGit2, Users, LinkIcon, UsersRound, Tags } from "lucide-react";
import {
getSubmissionStats,
getTrackCounts,
getTeamSizeDistribution,
getSubmissionTimeline,
} from "@/lib/queries";
import { TrackChart, TimelineChart, TeamSizeChart } from "@/components/admin/charts";

export const metadata = { title: "Admin Β· Stats" };

function StatCard({
label,
value,
hint,
icon: Icon,
}: {
label: string;
value: string | number;
hint?: string;
icon: React.ComponentType<{ className?: string }>;
}) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
<Icon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-3xl font-semibold tracking-tight">{value}</div>
{hint && <p className="mt-1 text-xs text-muted-foreground">{hint}</p>}
</CardContent>
</Card>
);
}

export default async function StatsPage() {
const [stats, tracks, teamSizes, timeline] = await Promise.all([
getSubmissionStats(),
getTrackCounts(),
getTeamSizeDistribution(),
getSubmissionTimeline(),
]);

const devpostPct =
stats.totalProjects > 0
? Math.round((stats.withDevpost / stats.totalProjects) * 100)
: 0;

return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold tracking-tight">Submission stats</h2>
<p className="text-sm text-muted-foreground">
Live from the submissions database.
</p>
</div>

<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
<StatCard label="Projects" value={stats.totalProjects} icon={FolderGit2} />
<StatCard label="Hackers" value={stats.totalHackers} hint="across all teams" icon={Users} />
<StatCard
label="Avg team size"
value={stats.avgTeamSize}
hint="members per project"
icon={UsersRound}
/>
<StatCard label="Tracks" value={stats.distinctTracks} hint="distinct tracks entered" icon={Tags} />
<StatCard
label="Devpost links"
value={`${devpostPct}%`}
hint={`${stats.withDevpost} of ${stats.totalProjects} projects`}
icon={LinkIcon}
/>
</div>

<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Projects per track</CardTitle>
<CardDescription>Top tracks by number of submissions</CardDescription>
</CardHeader>
<CardContent>
<TrackChart data={tracks} />
</CardContent>
</Card>

<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Submissions over time</CardTitle>
<CardDescription>Cumulative submissions by hour</CardDescription>
</CardHeader>
<CardContent>
<TimelineChart data={timeline} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Team sizes</CardTitle>
<CardDescription>Projects by number of members</CardDescription>
</CardHeader>
<CardContent>
<TeamSizeChart data={teamSizes} />
</CardContent>
</Card>
</div>
</div>

<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
All tracks
<Badge variant="secondary">{tracks.length}</Badge>
</CardTitle>
<CardDescription>Full breakdown, including smaller tracks</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{tracks.map((t) => (
<Badge key={t.track} variant="outline" className="font-normal">
{t.track}
<span className="ml-1.5 text-muted-foreground">{t.count}</span>
</Badge>
))}
</div>
</CardContent>
</Card>
</div>
);
}
21 changes: 21 additions & 0 deletions client/app/admin/submissions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getProjects } from "@/lib/queries";
import { SubmissionsManager } from "@/components/admin/submissions-manager";

export const metadata = { title: "Admin Β· Submissions" };

export default async function SubmissionsPage() {
const projects = await getProjects();

return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold tracking-tight">Hacker submissions</h2>
<p className="text-sm text-muted-foreground">
Every project submitted. Edit details or remove a submission β€” changes
write straight to the database.
</p>
</div>
<SubmissionsManager initialProjects={projects} />
</div>
);
}
53 changes: 53 additions & 0 deletions client/app/api/admin/schedule/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { updateSlot, deleteSlot, type SlotUpdate } from "@/lib/schedule";

// PATCH /api/admin/schedule/[id] β€” edit a single slot (time / room / track /
// duration). Only provided fields change.
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
try {
const body = (await request.json()) as Record<string, unknown>;
const fields: SlotUpdate = {};
if (typeof body.room === "string" && body.room.trim()) fields.room = body.room.trim();
if (typeof body.track === "string") fields.track = body.track.trim() || null;
if (typeof body.scheduledAt === "string") fields.scheduledAt = body.scheduledAt;
if (typeof body.durationMinutes === "number") fields.durationMinutes = body.durationMinutes;

if (Object.keys(fields).length === 0) {
return Response.json({ ok: false, error: "No fields to update." }, { status: 400 });
}

const slot = await updateSlot(id, fields);
if (!slot) {
return Response.json({ ok: false, error: "Slot not found." }, { status: 404 });
}
return Response.json({ ok: true, slot });
} catch (error) {
return Response.json(
{ ok: false, error: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}

// DELETE /api/admin/schedule/[id] β€” remove a slot from the schedule.
export async function DELETE(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
try {
const deleted = await deleteSlot(id);
if (!deleted) {
return Response.json({ ok: false, error: "Slot not found." }, { status: 404 });
}
return Response.json({ ok: true });
} catch (error) {
return Response.json(
{ ok: false, error: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
Loading