diff --git a/CLAUDE.md b/CLAUDE.md index ee9e0b5..246dc85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,35 +1,41 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working with +code in this repository. ## Project Overview -Mad CSS is a TanStack Start application for "The Ultimate CSS Tournament" - an event website featuring 16 developers battling for CSS glory. Built with React 19, TanStack Router, and deploys to Cloudflare Workers. +Mad CSS is a TanStack Start application for "The Ultimate CSS Tournament" - an +event website featuring 16 developers battling for CSS glory. Built with React +19, TanStack Router, and deploys to Cloudflare Workers. ## Commands +**Package manager: pnpm** + ```bash # Development -npm run dev # Start dev server on port 3000 +pnpm dev # Start dev server on port 3000 +[Note] I will run the dev command myself unless otherwise specified # Build & Deploy -npm run build # Build for production -npm run deploy # Build and deploy to Cloudflare Workers +pnpm build # Build for production +pnpm deploy # Build and deploy to Cloudflare Workers # Code Quality -npm run check # Run Biome linter and formatter checks -npm run lint # Lint only -npm run format # Format only +pnpm check # Run Biome linter and formatter checks +pnpm lint # Lint only +pnpm format # Format only # Testing -npm run test # Run Vitest tests +pnpm test # Run Vitest tests # Database -npm run db:generate # Generate Drizzle migrations from schema -npm run db:migrate:local # Apply migrations to local D1 -npm run db:migrate:prod # Apply migrations to production D1 -npm run db:studio # Open Drizzle Studio -npm run db:setup # Generate + migrate local (full setup) +pnpm db:generate # Generate Drizzle migrations from schema +pnpm db:migrate:local # Apply migrations to local D1 +pnpm db:migrate:prod # Apply migrations to production D1 +pnpm db:studio # Open Drizzle Studio +pnpm db:setup # Generate + migrate local (full setup) ``` ## Database Setup @@ -66,6 +72,20 @@ BETTER_AUTH_SECRET=your_random_secret BETTER_AUTH_URL=http://localhost:3000 ``` +### GitHub OAuth Setup + +1. Go to GitHub Settings > Developer settings > OAuth Apps > New OAuth App +2. Fill in: + - **Application name:** Mad CSS (Local) or similar + - **Homepage URL:** `http://localhost:3000/test` + - **Authorization callback URL:** + `http://localhost:3000/api/auth/callback/github` +3. Click "Register application" +4. Copy the **Client ID** to `GITHUB_CLIENT_ID` in `.dev.vars` +5. Generate a new **Client Secret** and copy to `GITHUB_CLIENT_SECRET` + +The login flow redirects to `/test` after authentication. + ### Production Deployment 1. Set secrets in Cloudflare dashboard (Workers > Settings > Variables): @@ -82,21 +102,43 @@ BETTER_AUTH_URL=http://localhost:3000 **Stack:** TanStack Start (SSR framework) + React 19 + Vite + Cloudflare Workers -**File-based routing:** Routes live in `src/routes/`. TanStack Router auto-generates `src/routeTree.gen.ts` - don't edit this file manually. +**File-based routing:** Routes live in `src/routes/`. TanStack Router +auto-generates `src/routeTree.gen.ts` - don't edit this file manually. **Key directories:** - `src/routes/` - Page components and API routes - `src/routes/__root.tsx` - Root layout, includes Header and devtools -- `src/components/` - Reusable components (Header, Ticket, Roster) +- `src/components/` - Reusable components (Header, Ticket, LoginSection, + bracket/, roster/, footer/, rules/) +- `src/lib/` - Auth setup (better-auth) and utilities (cfImage.ts for Cloudflare + Images) +- `src/data/` - Player data (players.ts with 16 contestants) - `src/styles/` - CSS files imported directly into components -- `public/` - Static assets (logos, images) +- `public/` - Static assets (logos, images, card artwork) **Path alias:** `@/*` maps to `./src/*` -**Styling:** Plain CSS with CSS custom properties defined in `src/styles/styles.css`. Uses custom fonts (Kaltjer, CollegiateBlackFLF, Inter) and texture backgrounds. +**Styling:** Plain CSS with CSS custom properties defined in +`src/styles/styles.css`. Uses custom fonts (Kaltjer, CollegiateBlackFLF, Inter) +and texture backgrounds. ## Code Style - Biome for linting/formatting (tabs, double quotes) - TypeScript strict mode +- XY Flow library for tournament bracket visualization + +## Comment Policy + +### Unacceptable Comments + +- Comments that repeat what code does +- Commented-out code (delete it) +- Obvious comments ("increment counter") +- Comments instead of good naming + +### Principle + +Code should be self-documenting. If you need a comment to explain WHAT the code +does, consider refactoring to make it clearer. diff --git a/biome.json b/biome.json index cdfd60b..ab70548 100644 --- a/biome.json +++ b/biome.json @@ -24,7 +24,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "complexity": { + "noImportantStyles": "off" + } } }, "javascript": { diff --git a/drizzle/0000_rare_juggernaut.sql b/drizzle/0000_rare_juggernaut.sql deleted file mode 100644 index dc3063d..0000000 --- a/drizzle/0000_rare_juggernaut.sql +++ /dev/null @@ -1,53 +0,0 @@ -CREATE TABLE `account` ( - `id` text PRIMARY KEY NOT NULL, - `account_id` text NOT NULL, - `provider_id` text NOT NULL, - `user_id` text NOT NULL, - `access_token` text, - `refresh_token` text, - `id_token` text, - `access_token_expires_at` integer, - `refresh_token_expires_at` integer, - `scope` text, - `password` text, - `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint -CREATE TABLE `session` ( - `id` text PRIMARY KEY NOT NULL, - `expires_at` integer NOT NULL, - `token` text NOT NULL, - `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, - `updated_at` integer NOT NULL, - `ip_address` text, - `user_agent` text, - `user_id` text NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint -CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint -CREATE TABLE `user` ( - `id` text PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `email` text NOT NULL, - `email_verified` integer DEFAULT false NOT NULL, - `image` text, - `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, - `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint -CREATE TABLE `verification` ( - `id` text PRIMARY KEY NOT NULL, - `identifier` text NOT NULL, - `value` text NOT NULL, - `expires_at` integer NOT NULL, - `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, - `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL -); ---> statement-breakpoint -CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`); \ No newline at end of file diff --git a/drizzle/0000_wonderful_warpath.sql b/drizzle/0000_wonderful_warpath.sql new file mode 100644 index 0000000..8c18358 --- /dev/null +++ b/drizzle/0000_wonderful_warpath.sql @@ -0,0 +1,104 @@ +CREATE TABLE `account` ( + `id` text PRIMARY KEY NOT NULL, + `account_id` text NOT NULL, + `provider_id` text NOT NULL, + `user_id` text NOT NULL, + `access_token` text, + `refresh_token` text, + `id_token` text, + `access_token_expires_at` integer, + `refresh_token_expires_at` integer, + `scope` text, + `password` text, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint +CREATE TABLE `session` ( + `id` text PRIMARY KEY NOT NULL, + `expires_at` integer NOT NULL, + `token` text NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer NOT NULL, + `ip_address` text, + `user_agent` text, + `user_id` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint +CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint +CREATE TABLE `tournament_result` ( + `id` text PRIMARY KEY NOT NULL, + `game_id` text NOT NULL, + `winner_id` text NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `tournament_result_game_id_unique` ON `tournament_result` (`game_id`);--> statement-breakpoint +CREATE INDEX `tournament_result_gameId_idx` ON `tournament_result` (`game_id`);--> statement-breakpoint +CREATE TABLE `user` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL, + `email_verified` integer DEFAULT false NOT NULL, + `image` text, + `username` text, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);--> statement-breakpoint +CREATE TABLE `user_bracket_status` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `is_locked` integer DEFAULT false NOT NULL, + `locked_at` integer, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_bracket_status_user_id_unique` ON `user_bracket_status` (`user_id`);--> statement-breakpoint +CREATE INDEX `user_bracket_status_userId_idx` ON `user_bracket_status` (`user_id`);--> statement-breakpoint +CREATE TABLE `user_prediction` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `game_id` text NOT NULL, + `predicted_winner_id` text NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `user_prediction_userId_idx` ON `user_prediction` (`user_id`);--> statement-breakpoint +CREATE INDEX `user_prediction_userId_gameId_idx` ON `user_prediction` (`user_id`,`game_id`);--> statement-breakpoint +CREATE TABLE `user_score` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `round1_score` integer DEFAULT 0 NOT NULL, + `round2_score` integer DEFAULT 0 NOT NULL, + `round3_score` integer DEFAULT 0 NOT NULL, + `round4_score` integer DEFAULT 0 NOT NULL, + `total_score` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_score_user_id_unique` ON `user_score` (`user_id`);--> statement-breakpoint +CREATE INDEX `user_score_userId_idx` ON `user_score` (`user_id`);--> statement-breakpoint +CREATE INDEX `user_score_totalScore_idx` ON `user_score` (`total_score`);--> statement-breakpoint +CREATE TABLE `verification` ( + `id` text PRIMARY KEY NOT NULL, + `identifier` text NOT NULL, + `value` text NOT NULL, + `expires_at` integer NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 55bd2c8..779c3e8 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "8e24b20b-75e1-4780-815e-9b34da83ea0e", + "id": "0dd9c063-5d8e-49d0-bb27-76fd0763885a", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "account": { @@ -224,6 +224,68 @@ "uniqueConstraints": {}, "checkConstraints": {} }, + "tournament_result": { + "name": "tournament_result", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "game_id": { + "name": "game_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "winner_id": { + "name": "winner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "tournament_result_game_id_unique": { + "name": "tournament_result_game_id_unique", + "columns": [ + "game_id" + ], + "isUnique": true + }, + "tournament_result_gameId_idx": { + "name": "tournament_result_gameId_idx", + "columns": [ + "game_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, "user": { "name": "user", "columns": { @@ -263,6 +325,13 @@ "notNull": false, "autoincrement": false }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "integer", @@ -287,6 +356,13 @@ "email" ], "isUnique": true + }, + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true } }, "foreignKeys": {}, @@ -294,6 +370,282 @@ "uniqueConstraints": {}, "checkConstraints": {} }, + "user_bracket_status": { + "name": "user_bracket_status", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_locked": { + "name": "is_locked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "locked_at": { + "name": "locked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_bracket_status_user_id_unique": { + "name": "user_bracket_status_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "user_bracket_status_userId_idx": { + "name": "user_bracket_status_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_bracket_status_user_id_user_id_fk": { + "name": "user_bracket_status_user_id_user_id_fk", + "tableFrom": "user_bracket_status", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_prediction": { + "name": "user_prediction", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_id": { + "name": "game_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "predicted_winner_id": { + "name": "predicted_winner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_prediction_userId_idx": { + "name": "user_prediction_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_prediction_userId_gameId_idx": { + "name": "user_prediction_userId_gameId_idx", + "columns": [ + "user_id", + "game_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_prediction_user_id_user_id_fk": { + "name": "user_prediction_user_id_user_id_fk", + "tableFrom": "user_prediction", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_score": { + "name": "user_score", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "round1_score": { + "name": "round1_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "round2_score": { + "name": "round2_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "round3_score": { + "name": "round3_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "round4_score": { + "name": "round4_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_score": { + "name": "total_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_score_user_id_unique": { + "name": "user_score_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "user_score_userId_idx": { + "name": "user_score_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_score_totalScore_idx": { + "name": "user_score_totalScore_idx", + "columns": [ + "total_score" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_score_user_id_user_id_fk": { + "name": "user_score_user_id_user_id_fk", + "tableFrom": "user_score", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, "verification": { "name": "verification", "columns": { diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d434796..a8a2c0a 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1767919267096, - "tag": "0000_rare_juggernaut", + "when": 1769443421416, + "tag": "0000_wonderful_warpath", "breakpoints": true } ] diff --git a/package.json b/package.json index 05a8747..321369e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "tailwindcss": "^4.1.18", - "vite-tsconfig-paths": "^6.0.3" + "vite-tsconfig-paths": "^6.0.3", + "workers-og": "^0.0.27" }, "devDependencies": { "@biomejs/biome": "2.3.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d9bd84..ffb6735 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: vite-tsconfig-paths: specifier: ^6.0.3 version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + workers-og: + specifier: ^0.0.27 + version: 0.0.27 devDependencies: '@biomejs/biome': specifier: 2.3.11 @@ -1164,6 +1167,10 @@ packages: '@remix-run/node-fetch-server@0.8.1': resolution: {integrity: sha512-J1dev372wtJqmqn9U/qbpbZxbJSQrogNN2+Qv1lKlpATpe/WQ9aCZfl/xSb9d2Rgh1IyLSvNxZAXPZxruO6Xig==} + '@resvg/resvg-wasm@2.4.0': + resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==} + engines: {node: '>= 10'} + '@rolldown/pluginutils@1.0.0-beta.40': resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} @@ -1295,6 +1302,11 @@ packages: cpu: [x64] os: [win32] + '@shuding/opentype.js@1.4.0-beta.0': + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -1783,6 +1795,10 @@ packages: babel-dead-code-elimination@1.0.11: resolution: {integrity: sha512-mwq3W3e/pKSI6TG8lXMiDWvEi1VXYlSBlJlB3l+I0bAb5u1RNUl88udos85eOPNK3m5EXK9uO7d2g08pesTySQ==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1895,6 +1911,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-lite@1.0.30001762: resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} @@ -1951,9 +1970,26 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + + css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-gradient-parser@0.0.16: + resolution: {integrity: sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==} + engines: {node: '>=16'} + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + css-tree@3.1.0: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -2161,6 +2197,10 @@ packages: electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + emoji-regex-xs@2.0.1: + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} + engines: {node: '>=10.0.0'} + encoding-sniffer@0.2.1: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} @@ -2214,6 +2254,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -2246,6 +2289,9 @@ packages: picomatch: optional: true + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -2302,6 +2348,10 @@ packages: crossws: optional: true + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2389,6 +2439,9 @@ packages: engines: {node: '>=6'} hasBin: true + just-camel-case@6.2.0: + resolution: {integrity: sha512-ICenRLXwkQYLk3UyvLQZ+uKuwFVJ3JHFYFn7F2782G2Mv2hW8WPePqgdhpnjGaqkYtSVWnyCESZhGXUmY3/bEg==} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -2470,6 +2523,9 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lru-cache@11.2.4: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} @@ -2547,6 +2603,12 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -2576,6 +2638,9 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2654,6 +2719,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + satori@0.15.2: + resolution: {integrity: sha512-vu/49vdc8MzV5jUchs3TIRDCOkOvMc1iJ11MrZvhg9tE4ziKIEIBjBZvies6a9sfM2vQ2gc3dXeu6rCK7AztHA==} + engines: {node: '>=16'} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -2746,6 +2815,9 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} + string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2774,6 +2846,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -2857,6 +2932,9 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} @@ -3002,6 +3080,9 @@ packages: engines: {node: '>=16'} hasBin: true + workers-og@0.0.27: + resolution: {integrity: sha512-QvwptQ0twmouQHiITUi3kYxEPCLdueC/U4msQ2xMz2iktd+iseSs7zlREw3T1dAsPxPw73FQlw8cXFsfANZPlw==} + wrangler@4.58.0: resolution: {integrity: sha512-Jm6EYtlt8iUcznOCPSMYC54DYkwrMNESzbH0Vh3GFHv/7XVw5gBC13YJAB+nWMRGJ+6B2dMzy/NVQS4ONL51Pw==} engines: {node: '>=20.0.0'} @@ -3053,6 +3134,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yoga-wasm-web@0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} @@ -3804,6 +3888,8 @@ snapshots: '@remix-run/node-fetch-server@0.8.1': {} + '@resvg/resvg-wasm@2.4.0': {} + '@rolldown/pluginutils@1.0.0-beta.40': {} '@rolldown/pluginutils@1.0.0-beta.53': {} @@ -3883,6 +3969,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.55.1': optional: true + '@shuding/opentype.js@1.4.0-beta.0': + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + '@sindresorhus/is@7.2.0': {} '@solid-primitives/event-listener@2.4.3(solid-js@1.9.10)': @@ -4489,6 +4580,8 @@ snapshots: transitivePeerDependencies: - supports-color + base64-js@0.0.8: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.9.11: {} @@ -4570,6 +4663,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + camelize@1.0.1: {} + caniuse-lite@1.0.30001762: {} chai@6.2.2: {} @@ -4639,6 +4734,14 @@ snapshots: cookie@1.1.1: {} + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + + css-color-keywords@1.0.0: {} + + css-gradient-parser@0.0.16: {} + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -4647,6 +4750,12 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + css-tree@3.1.0: dependencies: mdn-data: 2.12.2 @@ -4761,6 +4870,8 @@ snapshots: electron-to-chromium@1.5.267: {} + emoji-regex-xs@2.0.1: {} + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 @@ -4904,6 +5015,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + esprima@4.0.1: {} estree-walker@3.0.3: @@ -4922,6 +5035,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.7.4: {} + file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -4962,6 +5077,8 @@ snapshots: rou3: 0.7.12 srvx: 0.10.0 + hex-rgb@4.3.0: {} + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.8.0 @@ -5059,6 +5176,8 @@ snapshots: json5@2.2.3: {} + just-camel-case@6.2.0: {} + kleur@4.1.5: {} kysely@0.28.9: {} @@ -5117,6 +5236,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lru-cache@11.2.4: {} lru-cache@5.1.1: @@ -5187,6 +5311,13 @@ snapshots: dependencies: wrappy: 1.0.2 + pako@0.2.9: {} + + parse-css-color@0.2.1: + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -5214,6 +5345,8 @@ snapshots: picomatch@4.0.3: {} + postcss-value-parser@4.2.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -5327,6 +5460,20 @@ snapshots: safer-buffer@2.1.2: {} + satori@0.15.2: + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-gradient-parser: 0.0.16 + css-to-react-native: 3.2.0 + emoji-regex-xs: 2.0.1 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-wasm-web: 0.3.3 + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -5418,6 +5565,8 @@ snapshots: stoppable@1.1.0: {} + string.prototype.codepointat@0.2.1: {} + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -5447,6 +5596,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tiny-inflate@1.0.3: {} + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -5511,6 +5662,11 @@ snapshots: dependencies: pathe: 2.0.3 + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -5632,6 +5788,13 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260107.1 '@cloudflare/workerd-windows-64': 1.20260107.1 + workers-og@0.0.27: + dependencies: + '@resvg/resvg-wasm': 2.4.0 + just-camel-case: 6.2.0 + satori: 0.15.2 + yoga-wasm-web: 0.3.3 + wrangler@4.58.0: dependencies: '@cloudflare/kv-asset-handler': 0.4.1 @@ -5667,6 +5830,8 @@ snapshots: yallist@3.1.1: {} + yoga-wasm-web@0.3.3: {} + youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.3 diff --git a/public/fonts/DSEG7Classic-Bold.woff2 b/public/fonts/DSEG7Classic-Bold.woff2 new file mode 100644 index 0000000..558eec4 Binary files /dev/null and b/public/fonts/DSEG7Classic-Bold.woff2 differ diff --git a/scripts/seed-leaderboard.sql b/scripts/seed-leaderboard.sql new file mode 100644 index 0000000..d5fd3e7 --- /dev/null +++ b/scripts/seed-leaderboard.sql @@ -0,0 +1,110 @@ +-- Seed tournament results (Round 1 only for testing) +INSERT OR REPLACE INTO tournament_result (id, game_id, winner_id, created_at, updated_at) VALUES + ('tr-r1-0', 'r1-0', 'wes-bos', 1737331200000, 1737331200000), + ('tr-r1-1', 'r1-1', 'kevin-powell', 1737331200000, 1737331200000), + ('tr-r1-2', 'r1-2', 'adam-wathan', 1737331200000, 1737331200000), + ('tr-r1-3', 'r1-3', 'josh-comeau', 1737331200000, 1737331200000), + ('tr-r1-4', 'r1-4', 'kyle-cook', 1737331200000, 1737331200000), + ('tr-r1-5', 'r1-5', 'bramus', 1737331200000, 1737331200000), + ('tr-r1-6', 'r1-6', 'cassie-evans', 1737331200000, 1737331200000), + ('tr-r1-7', 'r1-7', 'miriam-suzanne', 1737331200000, 1737331200000); + +-- Add some quarterfinal results +INSERT OR REPLACE INTO tournament_result (id, game_id, winner_id, created_at, updated_at) VALUES + ('tr-qf-0', 'qf-0', 'kevin-powell', 1737331200000, 1737331200000), + ('tr-qf-1', 'qf-1', 'josh-comeau', 1737331200000, 1737331200000); + +-- Create dummy test users (for demo purposes) +-- Note: In production, users come from GitHub OAuth +INSERT OR REPLACE INTO user (id, name, email, email_verified, image, created_at, updated_at) VALUES + ('test-user-1', 'CSS Wizard', 'wizard@test.com', 1, 'https://avatars.githubusercontent.com/u/1?v=4', 1737331200000, 1737331200000), + ('test-user-2', 'Flexbox Fan', 'flex@test.com', 1, 'https://avatars.githubusercontent.com/u/2?v=4', 1737331200000, 1737331200000), + ('test-user-3', 'Grid Master', 'grid@test.com', 1, 'https://avatars.githubusercontent.com/u/3?v=4', 1737331200000, 1737331200000), + ('test-user-4', 'Animation Ace', 'anim@test.com', 1, 'https://avatars.githubusercontent.com/u/4?v=4', 1737331200000, 1737331200000), + ('test-user-5', 'Selector Savant', 'select@test.com', 1, 'https://avatars.githubusercontent.com/u/5?v=4', 1737331200000, 1737331200000); + +-- Mark test users' brackets as locked +INSERT OR REPLACE INTO user_bracket_status (id, user_id, is_locked, locked_at, created_at) VALUES + ('bs-1', 'test-user-1', 1, 1737331200000, 1737331200000), + ('bs-2', 'test-user-2', 1, 1737331200000, 1737331200000), + ('bs-3', 'test-user-3', 1, 1737331200000, 1737331200000), + ('bs-4', 'test-user-4', 1, 1737331200000, 1737331200000), + ('bs-5', 'test-user-5', 1, 1737331200000, 1737331200000); + +-- User 1 predictions (perfect Round 1, 1/2 QF = 80 + 20 = 100) +INSERT OR REPLACE INTO user_prediction (id, user_id, game_id, predicted_winner_id, created_at, updated_at) VALUES + ('p1-r1-0', 'test-user-1', 'r1-0', 'wes-bos', 1737331200000, 1737331200000), + ('p1-r1-1', 'test-user-1', 'r1-1', 'kevin-powell', 1737331200000, 1737331200000), + ('p1-r1-2', 'test-user-1', 'r1-2', 'adam-wathan', 1737331200000, 1737331200000), + ('p1-r1-3', 'test-user-1', 'r1-3', 'josh-comeau', 1737331200000, 1737331200000), + ('p1-r1-4', 'test-user-1', 'r1-4', 'kyle-cook', 1737331200000, 1737331200000), + ('p1-r1-5', 'test-user-1', 'r1-5', 'bramus', 1737331200000, 1737331200000), + ('p1-r1-6', 'test-user-1', 'r1-6', 'cassie-evans', 1737331200000, 1737331200000), + ('p1-r1-7', 'test-user-1', 'r1-7', 'miriam-suzanne', 1737331200000, 1737331200000), + ('p1-qf-0', 'test-user-1', 'qf-0', 'kevin-powell', 1737331200000, 1737331200000), + ('p1-qf-1', 'test-user-1', 'qf-1', 'adam-wathan', 1737331200000, 1737331200000); + +-- User 2 predictions (6/8 Round 1, 2/2 QF = 60 + 40 = 100) +INSERT OR REPLACE INTO user_prediction (id, user_id, game_id, predicted_winner_id, created_at, updated_at) VALUES + ('p2-r1-0', 'test-user-2', 'r1-0', 'wes-bos', 1737331200000, 1737331200000), + ('p2-r1-1', 'test-user-2', 'r1-1', 'kevin-powell', 1737331200000, 1737331200000), + ('p2-r1-2', 'test-user-2', 'r1-2', 'ania-kubow', 1737331200000, 1737331200000), + ('p2-r1-3', 'test-user-2', 'r1-3', 'josh-comeau', 1737331200000, 1737331200000), + ('p2-r1-4', 'test-user-2', 'r1-4', 'kyle-cook', 1737331200000, 1737331200000), + ('p2-r1-5', 'test-user-2', 'r1-5', 'bramus', 1737331200000, 1737331200000), + ('p2-r1-6', 'test-user-2', 'r1-6', 'stephanie-eckles', 1737331200000, 1737331200000), + ('p2-r1-7', 'test-user-2', 'r1-7', 'miriam-suzanne', 1737331200000, 1737331200000), + ('p2-qf-0', 'test-user-2', 'qf-0', 'kevin-powell', 1737331200000, 1737331200000), + ('p2-qf-1', 'test-user-2', 'qf-1', 'josh-comeau', 1737331200000, 1737331200000); + +-- User 3 predictions (5/8 Round 1, 1/2 QF = 50 + 20 = 70) +INSERT OR REPLACE INTO user_prediction (id, user_id, game_id, predicted_winner_id, created_at, updated_at) VALUES + ('p3-r1-0', 'test-user-3', 'r1-0', 'scott-tolinski', 1737331200000, 1737331200000), + ('p3-r1-1', 'test-user-3', 'r1-1', 'kevin-powell', 1737331200000, 1737331200000), + ('p3-r1-2', 'test-user-3', 'r1-2', 'adam-wathan', 1737331200000, 1737331200000), + ('p3-r1-3', 'test-user-3', 'r1-3', 'josh-comeau', 1737331200000, 1737331200000), + ('p3-r1-4', 'test-user-3', 'r1-4', 'jen-simmons', 1737331200000, 1737331200000), + ('p3-r1-5', 'test-user-3', 'r1-5', 'rachel-andrew', 1737331200000, 1737331200000), + ('p3-r1-6', 'test-user-3', 'r1-6', 'cassie-evans', 1737331200000, 1737331200000), + ('p3-r1-7', 'test-user-3', 'r1-7', 'miriam-suzanne', 1737331200000, 1737331200000), + ('p3-qf-0', 'test-user-3', 'qf-0', 'wes-bos', 1737331200000, 1737331200000), + ('p3-qf-1', 'test-user-3', 'qf-1', 'josh-comeau', 1737331200000, 1737331200000); + +-- User 4 predictions (4/8 Round 1, 0/2 QF = 40 + 0 = 40) +INSERT OR REPLACE INTO user_prediction (id, user_id, game_id, predicted_winner_id, created_at, updated_at) VALUES + ('p4-r1-0', 'test-user-4', 'r1-0', 'wes-bos', 1737331200000, 1737331200000), + ('p4-r1-1', 'test-user-4', 'r1-1', 'adam-argyle', 1737331200000, 1737331200000), + ('p4-r1-2', 'test-user-4', 'r1-2', 'adam-wathan', 1737331200000, 1737331200000), + ('p4-r1-3', 'test-user-4', 'r1-3', 'cassidy-williams', 1737331200000, 1737331200000), + ('p4-r1-4', 'test-user-4', 'r1-4', 'kyle-cook', 1737331200000, 1737331200000), + ('p4-r1-5', 'test-user-4', 'r1-5', 'rachel-andrew', 1737331200000, 1737331200000), + ('p4-r1-6', 'test-user-4', 'r1-6', 'stephanie-eckles', 1737331200000, 1737331200000), + ('p4-r1-7', 'test-user-4', 'r1-7', 'css-ninja', 1737331200000, 1737331200000), + ('p4-qf-0', 'test-user-4', 'qf-0', 'wes-bos', 1737331200000, 1737331200000), + ('p4-qf-1', 'test-user-4', 'qf-1', 'adam-wathan', 1737331200000, 1737331200000); + +-- User 5 predictions (3/8 Round 1, 1/2 QF = 30 + 20 = 50) +INSERT OR REPLACE INTO user_prediction (id, user_id, game_id, predicted_winner_id, created_at, updated_at) VALUES + ('p5-r1-0', 'test-user-5', 'r1-0', 'scott-tolinski', 1737331200000, 1737331200000), + ('p5-r1-1', 'test-user-5', 'r1-1', 'adam-argyle', 1737331200000, 1737331200000), + ('p5-r1-2', 'test-user-5', 'r1-2', 'ania-kubow', 1737331200000, 1737331200000), + ('p5-r1-3', 'test-user-5', 'r1-3', 'josh-comeau', 1737331200000, 1737331200000), + ('p5-r1-4', 'test-user-5', 'r1-4', 'jen-simmons', 1737331200000, 1737331200000), + ('p5-r1-5', 'test-user-5', 'r1-5', 'bramus', 1737331200000, 1737331200000), + ('p5-r1-6', 'test-user-5', 'r1-6', 'stephanie-eckles', 1737331200000, 1737331200000), + ('p5-r1-7', 'test-user-5', 'r1-7', 'css-ninja', 1737331200000, 1737331200000), + ('p5-qf-0', 'test-user-5', 'qf-0', 'kevin-powell', 1737331200000, 1737331200000), + ('p5-qf-1', 'test-user-5', 'qf-1', 'adam-wathan', 1737331200000, 1737331200000); + +-- Insert user scores (calculated based on above predictions) +-- User 1: R1=80, R2=20 (1 correct of 2 played), Total=100 +-- User 2: R1=60, R2=40 (2 correct), Total=100 +-- User 3: R1=50, R2=20 (1 correct), Total=70 +-- User 4: R1=40, R2=0, Total=40 +-- User 5: R1=30, R2=20 (1 correct), Total=50 +INSERT OR REPLACE INTO user_score (id, user_id, round1_score, round2_score, round3_score, round4_score, total_score, created_at, updated_at) VALUES + ('score-1', 'test-user-1', 80, 20, 0, 0, 100, 1737331200000, 1737331200000), + ('score-2', 'test-user-2', 60, 40, 0, 0, 100, 1737331200000, 1737331200000), + ('score-3', 'test-user-3', 50, 20, 0, 0, 70, 1737331200000, 1737331200000), + ('score-5', 'test-user-5', 30, 20, 0, 0, 50, 1737331200000, 1737331200000), + ('score-4', 'test-user-4', 40, 0, 0, 0, 40, 1737331200000, 1737331200000); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 3cef0ab..5711497 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,7 +6,6 @@ export function Header() { return (
- {/* TODO this should be a vector image */} Mad CSS Logo
diff --git a/src/components/LoginSection.tsx b/src/components/LoginSection.tsx index 08b68c9..bce02b1 100644 --- a/src/components/LoginSection.tsx +++ b/src/components/LoginSection.tsx @@ -1,55 +1,322 @@ +import { useState } from "react"; +import { TOTAL_GAMES } from "@/data/players"; +import { useCountdown } from "@/hooks/useCountdown"; import { authClient } from "@/lib/auth-client"; +import { Scoreboard } from "./scoreboard/Scoreboard"; import "@/styles/login.css"; -export function LoginSection() { +export interface LoginSectionProps { + pickCount?: number; + isLocked?: boolean; + isSaving?: boolean; + hasChanges?: boolean; + error?: string | null; + deadline?: string; + isDeadlinePassed?: boolean; + username?: string | null; + onSave?: () => void; + onLock?: () => void; + onReset?: () => void; +} + +export function LoginSection({ + pickCount = 0, + isLocked = false, + isSaving = false, + hasChanges = false, + error = null, + deadline, + isDeadlinePassed = false, + username = null, + onSave, + onLock, + onReset, +}: LoginSectionProps) { const { data: session, isPending } = authClient.useSession(); + const [showLockConfirm, setShowLockConfirm] = useState(false); + const [copied, setCopied] = useState(false); + const countdown = useCountdown(deadline); + const isUrgent = + countdown.totalMs > 0 && countdown.totalMs < 24 * 60 * 60 * 1000; + + const shareUrl = username + ? `${typeof window !== "undefined" ? window.location.origin : ""}/bracket/${username}` + : null; + + const handleCopyLink = async () => { + if (!shareUrl) return; + try { + await navigator.clipboard.writeText(shareUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback for older browsers + const input = document.createElement("input"); + input.value = shareUrl; + document.body.appendChild(input); + input.select(); + document.execCommand("copy"); + document.body.removeChild(input); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + const twitterShareUrl = username + ? `https://twitter.com/intent/tweet?text=${encodeURIComponent(`Check out my March Mad CSS bracket picks! \ud83c\udfc0\n\n${shareUrl}`)}` + : null; if (isPending) { return (
- ... + Loading...
); } if (session?.user) { + const canLock = pickCount === TOTAL_GAMES && !isLocked && !isDeadlinePassed; + return (
- {session.user.name} -
+ {/* Header: Avatar + Name + Sign Out */} +
+

- You're in, {session.user.name}! + Welcome back, {session.user.name}

-

Lock in your picks below.

+
- + + {/* Status badges for locked/deadline states */} + {isLocked && ( + <> +
+ ✓ Your bracket is locked in! +
+ + {/* Share section - only show when locked and username exists */} + {shareUrl && ( +
+
+ + Share your bracket +
+
+ + {twitterShareUrl && ( + + + Share on X + + )} +
+
+ )} + + )} + + {isDeadlinePassed && !isLocked && ( +
Deadline has passed
+ )} + + {/* Progress section - only show when not locked */} + {!isLocked && !isDeadlinePassed && ( + <> +
+
+
+ {pickCount} / {TOTAL_GAMES} picks +
+ {deadline && countdown.totalMs > 0 && ( + + )} +
+
+ + {/* Instructions */} +
+ + + Click any player to pick them as the winner of that match + +
+ + {/* Actions */} +
+ {showLockConfirm ? ( +
+

Lock your bracket? This cannot be undone.

+
+ + +
+
+ ) : ( + <> + + + {pickCount > 0 && ( + + )} + + )} +
+ + )} + + {error &&

{error}

}
); } + // Logged out state return (
-
-

Think you can call it?

-

- Lock in your predictions before Round 1 and compete - for mass internet clout. Perfect bracket = mass internet clout. -

-
+

Think you can call it?

+

+ Lock in your predictions before Round 1 and compete for + mass internet clout. Perfect bracket = mass internet clout. +

+ {deadline && countdown.totalMs > 0 && ( + + )}