Skip to content
Open
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
2 changes: 2 additions & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

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

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>
</div>
);
}
4 changes: 2 additions & 2 deletions client/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PortalStub } from "@/components/portal-stub";
import { redirect } from "next/navigation";

export default function AdminPage() {
return <PortalStub title="Admin" />;
redirect("/admin/stats");
}
22 changes: 22 additions & 0 deletions client/app/admin/schedule/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getProjects } from "@/lib/queries";
import { deriveSchedule } 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();
const slots = deriveSchedule(projects);

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} />
</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>
);
}
19 changes: 19 additions & 0 deletions client/app/api/db-tables/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getDatabaseTables } from "@/lib/projects";

// GET /api/db-tables - lists user tables and columns in the connected Neon DB.
export async function GET() {
try {
const tables = await getDatabaseTables();

return Response.json({
ok: true,
count: tables.length,
tables,
});
} catch (error) {
return Response.json(
{ ok: false, error: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
21 changes: 21 additions & 0 deletions client/app/api/projects/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getProjects } from "@/lib/projects";

// GET /api/projects - returns all project-like rows from Neon.
export async function GET() {
try {
const result = await getProjects();

return Response.json({
ok: true,
count: result.projects.length,
sourceTable: result.sourceTable,
availableTables: result.availableTables,
projects: result.projects,
});
} catch (error) {
return Response.json(
{ ok: false, error: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
33 changes: 21 additions & 12 deletions client/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

@custom-variant dark (&:is(.dark *));

@theme {
--color-neutral-color: #6b7280;
--color-tertiary-color: #1a1a1b;
--color-secondary-color: #f8f9fa;
--color-primary-color: #e60000;

--color-background-color: #e6e6e6;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
Expand Down Expand Up @@ -53,32 +62,32 @@
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.50 0.17 215);
--primary: oklch(0.5 0.17 215);
--primary-foreground: oklch(0.98 0.01 220);
--secondary: oklch(0.967 0.006 220);
--secondary-foreground: oklch(0.21 0.015 215);
--muted: oklch(0.967 0.006 220);
--muted-foreground: oklch(0.50 0.020 215);
--muted-foreground: oklch(0.5 0.02 215);
--accent: oklch(0.967 0.006 220);
--accent-foreground: oklch(0.21 0.015 215);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.90 0.010 220);
--input: oklch(0.90 0.010 220);
--ring: oklch(0.50 0.17 215);
--border: oklch(0.9 0.01 220);
--input: oklch(0.9 0.01 220);
--ring: oklch(0.5 0.17 215);
--chart-1: oklch(0.82 0.14 200);
--chart-2: oklch(0.72 0.16 203);
--chart-3: oklch(0.62 0.17 205);
--chart-4: oklch(0.52 0.18 208);
--chart-5: oklch(0.50 0.17 215);
--chart-5: oklch(0.5 0.17 215);
--radius: 0.625rem;
--sidebar: oklch(0.985 0.002 220);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.50 0.17 215);
--sidebar-primary: oklch(0.5 0.17 215);
--sidebar-primary-foreground: oklch(0.98 0.01 220);
--sidebar-accent: oklch(0.967 0.006 220);
--sidebar-accent-foreground: oklch(0.21 0.015 215);
--sidebar-border: oklch(0.90 0.010 220);
--sidebar-ring: oklch(0.50 0.17 215);
--sidebar-border: oklch(0.9 0.01 220);
--sidebar-ring: oklch(0.5 0.17 215);
}

.dark {
Expand All @@ -89,7 +98,7 @@
--popover: oklch(0.216 0.006 56.043);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.64 0.17 215);
--primary-foreground: oklch(0.20 0.06 215);
--primary-foreground: oklch(0.2 0.06 215);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.268 0.007 34.298);
Expand All @@ -104,11 +113,11 @@
--chart-2: oklch(0.72 0.16 203);
--chart-3: oklch(0.62 0.17 205);
--chart-4: oklch(0.56 0.17 215);
--chart-5: oklch(0.50 0.17 215);
--chart-5: oklch(0.5 0.17 215);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.64 0.17 215);
--sidebar-primary-foreground: oklch(0.20 0.06 215);
--sidebar-primary-foreground: oklch(0.2 0.06 215);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
Expand Down
38 changes: 38 additions & 0 deletions client/app/hacker/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Link from "next/link";

export function Navbar() {
const left_navbar = [{ label: "Home", href: "/hacker" }] as const;

const right_navbar = [
{ label: "Submission", href: "/hacker/submission" },
{ label: "Projects", href: "/hacker/projects" },
{ label: "Schedule", href: "/hacker/schedule" },
] as const;

return (
<nav className="w-screen h-[10%] flex justify-between p-4 bg-background-color">
<div>
{left_navbar.map((portal) => (
<Link
key={portal.href}
href={portal.href}
className="text-neutral-color"
>
{portal.label}
</Link>
))}
</div>
<div className="flex gap-4 ">
{right_navbar.map((portal) => (
<Link
key={portal.href}
href={portal.href}
className="text-neutral-color"
>
{portal.label}
</Link>
))}
</div>
</nav>
);
}
37 changes: 37 additions & 0 deletions client/app/hacker/SideBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Link from "next/link";

const sidebarLinks = [
{ label: "Dashboard", href: "/hacker" },
{ label: "Schedule", href: "/hacker/schedule" },
{ label: "Food", href: "/hacker/food" },
{ label: "Location", href: "/hacker/location" },
{ label: "Submission", href: "/hacker/submission" },
{ label: "Projects", href: "/hacker/projects" },
] as const;

export function SideBar() {
return (
<aside className="flex w-full shrink-0 flex-col bg-[#0099CC] p-4 md:h-screen md:w-64">
<div className="border-b border-white/25 pb-4 md:pb-6">
<p className="text-lg font-semibold">HackCanada</p>
<p className="text-sm text-white/80">Hacker Portal</p>
</div>

<nav className="mt-4 flex flex-row flex-wrap gap-2 md:mt-6 md:flex-1 md:flex-col">
{sidebarLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="rounded-md px-3 py-2 text-sm font-medium text-white/90 transition-colors hover:bg-white/15 hover:text-white"
>
{link.label}
</Link>
))}
</nav>

<div className="mt-4 hidden border-t border-white/25 pt-4 text-sm text-white/75 md:block">
Need help?
</div>
</aside>
);
}
Binary file added client/app/hacker/assets/hackjudge1.glb
Binary file not shown.
Loading