Skip to content

Commit 0d701c0

Browse files
authored
Merge pull request #25 from Hack-Canada/development
WIP: Client app — shadcn UI + Neon Postgres
2 parents 598e2e3 + cab57c3 commit 0d701c0

325 files changed

Lines changed: 30885 additions & 27736 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.DS_Store

6 KB
Binary file not shown.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.cursor/
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
# dependencies
44
/node_modules
5-
package-lock.json
65
/.pnp
76
.pnp.*
87
.yarn/*
@@ -33,6 +32,7 @@ yarn-error.log*
3332

3433
# env files (can opt-in for committing if needed)
3534
.env*
35+
.env.local
3636

3737
# vercel
3838
.vercel
@@ -41,5 +41,4 @@ yarn-error.log*
4141
*.tsbuildinfo
4242
next-env.d.ts
4343

44-
#env
45-
.env.local
44+
/scripts

client/AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- BEGIN:nextjs-agent-rules -->
2+
# This is NOT the Next.js you know
3+
4+
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.
5+
<!-- END:nextjs-agent-rules -->

client/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md
Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
1818

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

21-
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.
22-
23-
## Pre-commit hooks
24-
25-
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.
26-
27-
**If you want to use it:**
28-
29-
1. Install the pre-commit dependency (from the **repo root**, not the frontend folder):
30-
31-
```bash
32-
cd .. # if you're in frontend/
33-
pip install -r requirements-dev.txt
34-
```
35-
36-
Or install pre-commit directly:
37-
38-
```bash
39-
pip install pre-commit
40-
```
41-
42-
2. Install the git hooks (from the repo root):
43-
44-
```bash
45-
pre-commit install
46-
```
47-
48-
3. After that, every `git commit` will run the hooks automatically. To run them once without committing:
49-
50-
```bash
51-
pre-commit run --all-files
52-
```
53-
54-
The first run may take a minute while pre-commit downloads the Gitleaks environment; later runs are fast.
21+
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.
5522

5623
## Learn More
5724

client/app/admin/layout.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { AdminNav } from "@/components/admin/admin-nav";
2+
import { Toaster } from "@/components/ui/sonner";
3+
4+
export default function AdminLayout({
5+
children,
6+
}: {
7+
children: React.ReactNode;
8+
}) {
9+
return (
10+
<div className="flex min-h-full flex-1 flex-col bg-background text-foreground">
11+
<header className="border-b bg-card">
12+
<div className="mx-auto w-full max-w-6xl px-6 py-4">
13+
<div className="mb-3 flex items-baseline gap-3">
14+
<h1 className="text-lg font-semibold tracking-tight">Admin</h1>
15+
<span className="text-sm text-muted-foreground">
16+
HackCanada Judging Platform
17+
</span>
18+
</div>
19+
<AdminNav />
20+
</div>
21+
</header>
22+
<main className="mx-auto w-full max-w-6xl flex-1 px-6 py-8">
23+
{children}
24+
</main>
25+
<Toaster />
26+
</div>
27+
);
28+
}

client/app/admin/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { redirect } from "next/navigation";
2+
3+
export default function AdminPage() {
4+
redirect("/admin/stats");
5+
}

client/app/admin/schedule/page.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { getProjects } from "@/lib/queries";
2+
import {
3+
seedScheduleIfEmpty,
4+
getScheduleSlots,
5+
DEFAULT_ROOMS,
6+
} from "@/lib/schedule";
7+
import { ScheduleManager } from "@/components/admin/schedule-manager";
8+
9+
export const metadata = { title: "Admin · Schedule" };
10+
11+
export default async function SchedulePage() {
12+
const projects = await getProjects();
13+
// Seed the schedule from the derived draft on first visit, then read it back.
14+
await seedScheduleIfEmpty(projects);
15+
const slots = await getScheduleSlots();
16+
17+
return (
18+
<div className="space-y-6">
19+
<div>
20+
<h2 className="text-xl font-semibold tracking-tight">
21+
Schedule manager
22+
</h2>
23+
<p className="text-sm text-muted-foreground">
24+
Override pitch times, add delays, and search across all projects.
25+
</p>
26+
</div>
27+
<ScheduleManager
28+
initialSlots={slots}
29+
rooms={DEFAULT_ROOMS}
30+
projects={projects.map((p) => ({
31+
id: p.id,
32+
name: p.project_name,
33+
track: p.tracks[0] ?? "General",
34+
}))}
35+
/>
36+
</div>
37+
);
38+
}

client/app/admin/stats/page.tsx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2+
import { Badge } from "@/components/ui/badge";
3+
import { FolderGit2, Users, LinkIcon, UsersRound, Tags } from "lucide-react";
4+
import {
5+
getSubmissionStats,
6+
getTrackCounts,
7+
getTeamSizeDistribution,
8+
getSubmissionTimeline,
9+
} from "@/lib/queries";
10+
import { TrackChart, TimelineChart, TeamSizeChart } from "@/components/admin/charts";
11+
12+
export const metadata = { title: "Admin · Stats" };
13+
14+
function StatCard({
15+
label,
16+
value,
17+
hint,
18+
icon: Icon,
19+
}: {
20+
label: string;
21+
value: string | number;
22+
hint?: string;
23+
icon: React.ComponentType<{ className?: string }>;
24+
}) {
25+
return (
26+
<Card>
27+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
28+
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
29+
<Icon className="size-4 text-muted-foreground" />
30+
</CardHeader>
31+
<CardContent>
32+
<div className="text-3xl font-semibold tracking-tight">{value}</div>
33+
{hint && <p className="mt-1 text-xs text-muted-foreground">{hint}</p>}
34+
</CardContent>
35+
</Card>
36+
);
37+
}
38+
39+
export default async function StatsPage() {
40+
const [stats, tracks, teamSizes, timeline] = await Promise.all([
41+
getSubmissionStats(),
42+
getTrackCounts(),
43+
getTeamSizeDistribution(),
44+
getSubmissionTimeline(),
45+
]);
46+
47+
const devpostPct =
48+
stats.totalProjects > 0
49+
? Math.round((stats.withDevpost / stats.totalProjects) * 100)
50+
: 0;
51+
52+
return (
53+
<div className="space-y-6">
54+
<div>
55+
<h2 className="text-xl font-semibold tracking-tight">Submission stats</h2>
56+
<p className="text-sm text-muted-foreground">
57+
Live from the submissions database.
58+
</p>
59+
</div>
60+
61+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
62+
<StatCard label="Projects" value={stats.totalProjects} icon={FolderGit2} />
63+
<StatCard label="Hackers" value={stats.totalHackers} hint="across all teams" icon={Users} />
64+
<StatCard
65+
label="Avg team size"
66+
value={stats.avgTeamSize}
67+
hint="members per project"
68+
icon={UsersRound}
69+
/>
70+
<StatCard label="Tracks" value={stats.distinctTracks} hint="distinct tracks entered" icon={Tags} />
71+
<StatCard
72+
label="Devpost links"
73+
value={`${devpostPct}%`}
74+
hint={`${stats.withDevpost} of ${stats.totalProjects} projects`}
75+
icon={LinkIcon}
76+
/>
77+
</div>
78+
79+
<div className="grid gap-6 lg:grid-cols-2">
80+
<Card>
81+
<CardHeader>
82+
<CardTitle>Projects per track</CardTitle>
83+
<CardDescription>Top tracks by number of submissions</CardDescription>
84+
</CardHeader>
85+
<CardContent>
86+
<TrackChart data={tracks} />
87+
</CardContent>
88+
</Card>
89+
90+
<div className="space-y-6">
91+
<Card>
92+
<CardHeader>
93+
<CardTitle>Submissions over time</CardTitle>
94+
<CardDescription>Cumulative submissions by hour</CardDescription>
95+
</CardHeader>
96+
<CardContent>
97+
<TimelineChart data={timeline} />
98+
</CardContent>
99+
</Card>
100+
<Card>
101+
<CardHeader>
102+
<CardTitle>Team sizes</CardTitle>
103+
<CardDescription>Projects by number of members</CardDescription>
104+
</CardHeader>
105+
<CardContent>
106+
<TeamSizeChart data={teamSizes} />
107+
</CardContent>
108+
</Card>
109+
</div>
110+
</div>
111+
112+
<Card>
113+
<CardHeader>
114+
<CardTitle className="flex items-center gap-2">
115+
All tracks
116+
<Badge variant="secondary">{tracks.length}</Badge>
117+
</CardTitle>
118+
<CardDescription>Full breakdown, including smaller tracks</CardDescription>
119+
</CardHeader>
120+
<CardContent>
121+
<div className="flex flex-wrap gap-2">
122+
{tracks.map((t) => (
123+
<Badge key={t.track} variant="outline" className="font-normal">
124+
{t.track}
125+
<span className="ml-1.5 text-muted-foreground">{t.count}</span>
126+
</Badge>
127+
))}
128+
</div>
129+
</CardContent>
130+
</Card>
131+
</div>
132+
);
133+
}

0 commit comments

Comments
 (0)