From d597c6545e300584496ff18a57ba68fb7bb88032 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Fri, 13 Mar 2026 17:07:06 +0100 Subject: [PATCH] use prettier formatting --- .prettierrc | 2 +- scripts/backfill-r2.ts | 48 +- scripts/cleanup-seed-from-prod.ts | 31 +- scripts/clear-db.ts | 4 +- scripts/seed-db.ts | 42 +- scripts/seed-staging.ts | 401 ++++++++++++++--- src/lib/components/Header.svelte | 62 ++- src/lib/components/Sidebar.svelte | 285 +++++++++--- src/lib/server/bancho/client.ts | 21 +- src/lib/server/bancho/dm-handler.ts | 20 +- src/lib/server/db/schema.ts | 101 +++-- src/lib/server/match/engine.ts | 24 +- src/lib/server/match/invites.ts | 3 +- src/lib/server/match/orchestrator.ts | 107 ++--- src/lib/server/match/queue.ts | 45 +- src/lib/server/match/rating.ts | 4 +- src/lib/server/notifications.ts | 7 +- src/lib/server/permissions.ts | 6 +- src/lib/server/storage/r2.ts | 8 +- src/lib/stores/queue.svelte.ts | 9 +- src/routes/(app)/+layout.svelte | 8 +- src/routes/(app)/+page.server.ts | 41 +- src/routes/(app)/+page.svelte | 184 +++++--- src/routes/(app)/admin/+page.server.ts | 34 +- src/routes/(app)/admin/+page.svelte | 77 ++-- src/routes/(app)/leaderboard/+page.server.ts | 15 +- src/routes/(app)/leaderboard/+page.svelte | 124 ++++-- src/routes/(app)/mappools/+page.server.ts | 2 +- src/routes/(app)/mappools/+page.svelte | 23 +- .../(app)/mappools/[id]/+page.server.ts | 25 +- src/routes/(app)/mappools/[id]/+page.svelte | 116 +++-- src/routes/(app)/matches/+page.server.ts | 6 +- src/routes/(app)/matches/+page.svelte | 220 +++++++--- src/routes/(app)/matches/[id]/+page.server.ts | 47 +- src/routes/(app)/matches/[id]/+page.svelte | 411 +++++++++++++----- src/routes/(app)/privacy/+page.svelte | 23 +- src/routes/(app)/settings/+page.server.ts | 4 +- src/routes/(app)/settings/+page.svelte | 36 +- src/routes/(app)/teams/+page.svelte | 55 ++- src/routes/(app)/teams/[id]/+page.server.ts | 4 +- src/routes/(app)/teams/[id]/+page.svelte | 41 +- src/routes/(app)/terms/+page.svelte | 8 +- src/routes/+error.svelte | 6 +- src/routes/api/health/+server.ts | 6 +- test/match/queue.test.ts | 24 +- 45 files changed, 1909 insertions(+), 861 deletions(-) diff --git a/.prettierrc b/.prettierrc index 819fa57..c9215a3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -12,5 +12,5 @@ } } ], - "tailwindStylesheet": "./src/routes/layout.css" + "tailwindStylesheet": "./src/routes/(app)/layout.css" } diff --git a/scripts/backfill-r2.ts b/scripts/backfill-r2.ts index 5d1c014..0c843aa 100644 --- a/scripts/backfill-r2.ts +++ b/scripts/backfill-r2.ts @@ -25,10 +25,19 @@ const OSU_CLIENT_ID = process.env.OSU_CLIENT_ID!; const OSU_CLIENT_SECRET = process.env.OSU_CLIENT_SECRET!; for (const [k, v] of Object.entries({ - R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, - R2_BUCKET, R2_PUBLIC_URL, DATABASE_URL, OSU_CLIENT_ID, OSU_CLIENT_SECRET + R2_ACCOUNT_ID, + R2_ACCESS_KEY_ID, + R2_SECRET_ACCESS_KEY, + R2_BUCKET, + R2_PUBLIC_URL, + DATABASE_URL, + OSU_CLIENT_ID, + OSU_CLIENT_SECRET })) { - if (!v) { console.error(`Missing env var: ${k}`); process.exit(1); } + if (!v) { + console.error(`Missing env var: ${k}`); + process.exit(1); + } } // ── R2 ───────────────────────────────────────────────────────────────── @@ -76,7 +85,9 @@ function isOsuUrl(url: string | null): boolean { } } -function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)); } +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} // ── osu! API ─────────────────────────────────────────────────────────── @@ -96,7 +107,7 @@ async function getOsuToken(): Promise { }) }); if (!res.ok) throw new Error(`osu! token failed: ${res.status}`); - const data = await res.json() as { access_token: string; expires_in: number }; + const data = (await res.json()) as { access_token: string; expires_in: number }; osuToken = data.access_token; osuTokenExpiry = Date.now() + data.expires_in * 1000; return osuToken; @@ -121,7 +132,7 @@ async function getBeatmap(beatmapId: string): Promise { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } }); if (!res.ok) return null; - return await res.json() as BeatmapData; + return (await res.json()) as BeatmapData; } catch { return null; } @@ -137,14 +148,17 @@ const sql = new SQL(DATABASE_URL); console.log('═══ 1/3: Backfilling mappool slot metadata + covers ═══\n'); -const slots = await sql<{ id: string; beatmap_id: string; cover_url: string | null; title: string | null }[]>` +const slots = await sql< + { id: string; beatmap_id: string; cover_url: string | null; title: string | null }[] +>` SELECT id, beatmap_id, cover_url, title FROM mappool_slot WHERE cover_url IS NULL OR title IS NULL `; console.log(`Found ${slots.length} slots missing metadata\n`); -let slotOk = 0, slotFail = 0; +let slotOk = 0, + slotFail = 0; for (const slot of slots) { process.stdout.write(` beatmap ${slot.beatmap_id} ... `); const bm = await getBeatmap(slot.beatmap_id); @@ -181,7 +195,9 @@ for (const slot of slots) { console.log(`\nSlots: ${slotOk} updated, ${slotFail} failed\n`); // Also update any slots that have osu! URLs instead of R2 URLs (partially migrated) -const osuUrlSlots = await sql<{ id: string; cover_url: string | null; list_cover_url: string | null }[]>` +const osuUrlSlots = await sql< + { id: string; cover_url: string | null; list_cover_url: string | null }[] +>` SELECT id, cover_url, list_cover_url FROM mappool_slot WHERE (cover_url LIKE '%ppy.sh%' OR list_cover_url LIKE '%ppy.sh%') `; @@ -189,8 +205,12 @@ const osuUrlSlots = await sql<{ id: string; cover_url: string | null; list_cover if (osuUrlSlots.length > 0) { console.log(`Found ${osuUrlSlots.length} slots with osu! URLs (need R2 proxy)\n`); for (const slot of osuUrlSlots) { - const newCover = slot.cover_url && isOsuUrl(slot.cover_url) ? await proxyToR2(slot.cover_url) : slot.cover_url; - const newList = slot.list_cover_url && isOsuUrl(slot.list_cover_url) ? await proxyToR2(slot.list_cover_url) : slot.list_cover_url; + const newCover = + slot.cover_url && isOsuUrl(slot.cover_url) ? await proxyToR2(slot.cover_url) : slot.cover_url; + const newList = + slot.list_cover_url && isOsuUrl(slot.list_cover_url) + ? await proxyToR2(slot.list_cover_url) + : slot.list_cover_url; await sql`UPDATE mappool_slot SET cover_url = ${newCover}, list_cover_url = ${newList} WHERE id = ${slot.id}`; } console.log('Done re-proxying slot covers\n'); @@ -206,7 +226,8 @@ const users = await sql<{ id: string; image: string | null }[]>` SELECT id, image FROM "user" WHERE image IS NOT NULL `; -let avatarOk = 0, avatarSkip = 0; +let avatarOk = 0, + avatarSkip = 0; for (const u of users) { if (!isOsuUrl(u.image)) { avatarSkip++; @@ -235,7 +256,8 @@ const teams = await sql<{ id: string; avatar_url: string | null }[]>` SELECT id, avatar_url FROM team WHERE avatar_url IS NOT NULL `; -let teamOk = 0, teamSkip = 0; +let teamOk = 0, + teamSkip = 0; for (const t of teams) { if (!isOsuUrl(t.avatar_url)) { teamSkip++; diff --git a/scripts/cleanup-seed-from-prod.ts b/scripts/cleanup-seed-from-prod.ts index 1c36acf..8dac1fe 100644 --- a/scripts/cleanup-seed-from-prod.ts +++ b/scripts/cleanup-seed-from-prod.ts @@ -25,8 +25,7 @@ async function count(query: string): Promise { async function exec(description: string, deleteSql: string) { // Count first - const countSql = deleteSql - .replace(/^DELETE FROM/, 'SELECT COUNT(*) as count FROM'); + const countSql = deleteSql.replace(/^DELETE FROM/, 'SELECT COUNT(*) as count FROM'); const n = await count(countSql); if (n === 0) { @@ -53,9 +52,7 @@ async function main() { const realUsers = await count( `SELECT COUNT(*) as count FROM "user" WHERE id NOT LIKE 'seed-user-%'` ); - const seedUsers = await count( - `SELECT COUNT(*) as count FROM "user" WHERE id LIKE 'seed-user-%'` - ); + const seedUsers = await count(`SELECT COUNT(*) as count FROM "user" WHERE id LIKE 'seed-user-%'`); console.log(` Real users: ${realUsers}`); console.log(` Seed users: ${seedUsers}\n`); @@ -168,37 +165,23 @@ async function main() { // 13. Teams console.log('Phase 13: Teams'); - await exec( - 'team (owned by seed users)', - `DELETE FROM team WHERE owner_id LIKE 'seed-user-%'` - ); + await exec('team (owned by seed users)', `DELETE FROM team WHERE owner_id LIKE 'seed-user-%'`); // 14. Sessions (cascade from user delete, but clean up explicitly) console.log('Phase 14: Sessions'); - await exec( - 'session (from seed users)', - `DELETE FROM session WHERE user_id LIKE 'seed-user-%'` - ); + await exec('session (from seed users)', `DELETE FROM session WHERE user_id LIKE 'seed-user-%'`); // 15. Accounts console.log('Phase 15: Accounts'); - await exec( - 'account (from seed users)', - `DELETE FROM account WHERE user_id LIKE 'seed-user-%'` - ); + await exec('account (from seed users)', `DELETE FROM account WHERE user_id LIKE 'seed-user-%'`); // 16. Users (the root) console.log('Phase 16: Users'); - await exec( - 'user (seed users)', - `DELETE FROM "user" WHERE id LIKE 'seed-user-%'` - ); + await exec('user (seed users)', `DELETE FROM "user" WHERE id LIKE 'seed-user-%'`); // ── Verify ── console.log('\n── Verification ──'); - const remaining = await count( - `SELECT COUNT(*) as count FROM "user" WHERE id LIKE 'seed-user-%'` - ); + const remaining = await count(`SELECT COUNT(*) as count FROM "user" WHERE id LIKE 'seed-user-%'`); const remainingMatches = await count( `SELECT COUNT(*) as count FROM match WHERE created_by LIKE 'seed-user-%'` ); diff --git a/scripts/clear-db.ts b/scripts/clear-db.ts index ec46267..ab9385e 100644 --- a/scripts/clear-db.ts +++ b/scripts/clear-db.ts @@ -38,7 +38,9 @@ async function main() { } if (flag === '--full') { - await db.execute(sql`DELETE FROM team_member WHERE team_id IN (SELECT id FROM team WHERE is_personal = false)`); + await db.execute( + sql`DELETE FROM team_member WHERE team_id IN (SELECT id FROM team WHERE is_personal = false)` + ); await db.execute(sql`DELETE FROM team WHERE is_personal = false`); console.log(' Cleared: non-personal teams'); diff --git a/scripts/seed-db.ts b/scripts/seed-db.ts index 0b45cde..422733f 100644 --- a/scripts/seed-db.ts +++ b/scripts/seed-db.ts @@ -42,9 +42,30 @@ const mappoolSlot = pgTable('mappool_slot', { // Sample beatmap IDs (real osu! maps) const SAMPLE_MAPS = [ { id: '75', category: 'NM', title: 'Disco Prince', artist: 'Kenji Ninuma', sr: 2.5, bpm: 120 }, - { id: '163', category: 'NM', title: 'Crazy Noisy Bizarre Town', artist: 'The DU', sr: 3.2, bpm: 140 }, - { id: '250', category: 'HD', title: 'Sakura Kagetsu', artist: 'Mutsuhiko Izumi', sr: 4.1, bpm: 165 }, - { id: '306', category: 'HR', title: 'Airman ga Taosenai', artist: 'Team Nekokan', sr: 5.0, bpm: 200 }, + { + id: '163', + category: 'NM', + title: 'Crazy Noisy Bizarre Town', + artist: 'The DU', + sr: 3.2, + bpm: 140 + }, + { + id: '250', + category: 'HD', + title: 'Sakura Kagetsu', + artist: 'Mutsuhiko Izumi', + sr: 4.1, + bpm: 165 + }, + { + id: '306', + category: 'HR', + title: 'Airman ga Taosenai', + artist: 'Team Nekokan', + sr: 5.0, + bpm: 200 + }, { id: '390', category: 'DT', title: 'Kira Kira Days', artist: 'MOSAIC.WAV', sr: 4.5, bpm: 175 }, { id: '455', category: 'FM', title: 'FREEDOM DiVE', artist: 'xi', sr: 6.0, bpm: 222 }, { id: '500', category: 'TB', title: 'Blue Zenith', artist: 'xi', sr: 6.5, bpm: 200 } @@ -61,10 +82,13 @@ async function main() { } // Create a sample mappool - const [pool] = await db.insert(mappool).values({ - name: 'Dev Test Pool', - verifiedAt: new Date() - }).returning(); + const [pool] = await db + .insert(mappool) + .values({ + name: 'Dev Test Pool', + verifiedAt: new Date() + }) + .returning(); console.log(`Created mappool: ${pool.name} (${pool.id})`); @@ -86,7 +110,9 @@ async function main() { }); } - console.log(` Added ${SAMPLE_MAPS.length} maps (${[...new Set(SAMPLE_MAPS.map((m) => m.category))].join(', ')})`); + console.log( + ` Added ${SAMPLE_MAPS.length} maps (${[...new Set(SAMPLE_MAPS.map((m) => m.category))].join(', ')})` + ); console.log('Done. Sign in via osu! OAuth to create a user and personal team.'); process.exit(0); } diff --git a/scripts/seed-staging.ts b/scripts/seed-staging.ts index 250f573..fefa377 100644 --- a/scripts/seed-staging.ts +++ b/scripts/seed-staging.ts @@ -258,8 +258,8 @@ function rankToElo(rank: number): number { function generateSyntheticRank(index: number, total: number): number { const p = index / total; if (p < 0.05) return randInt(1000, 10000); - if (p < 0.20) return randInt(10000, 50000); - if (p < 0.60) return randInt(50000, 300000); + if (p < 0.2) return randInt(10000, 50000); + if (p < 0.6) return randInt(50000, 300000); if (p < 0.85) return randInt(300000, 1000000); return randInt(1000000, 5000000); } @@ -286,20 +286,108 @@ function timer() { // ── Name generators ───────────────────────────────────────────────────── -const PREFIXES = [ - '', '', '', 'xx', 'xX', 'ii', 'oO', 'Xx', '-', '_', 'Mr', 'Dr', 'DJ' -]; +const PREFIXES = ['', '', '', 'xx', 'xX', 'ii', 'oO', 'Xx', '-', '_', 'Mr', 'Dr', 'DJ']; const WORDS = [ - 'Ace', 'Arrow', 'Blaze', 'Bolt', 'Cat', 'Cloud', 'Comet', 'Crow', 'Crystal', 'Cyber', - 'Dark', 'Dawn', 'Demon', 'Diamond', 'Dragon', 'Dream', 'Eagle', 'Echo', 'Edge', 'Elite', - 'Ember', 'Fang', 'Fire', 'Flash', 'Fox', 'Frost', 'Ghost', 'Gold', 'Hawk', 'Haze', - 'Hero', 'Hunt', 'Ice', 'Iron', 'Jade', 'Jet', 'King', 'Knight', 'Leo', 'Light', - 'Luna', 'Lynx', 'Mist', 'Moon', 'Neo', 'Night', 'Nova', 'Omega', 'Onyx', 'Phantom', - 'Phoenix', 'Pixel', 'Prime', 'Pulse', 'Rage', 'Rain', 'Raven', 'Rex', 'Rise', 'River', - 'Rock', 'Ruby', 'Rush', 'Sage', 'Shade', 'Shadow', 'Silver', 'Sky', 'Snap', 'Snow', - 'Solar', 'Soul', 'Spark', 'Star', 'Steel', 'Storm', 'Strike', 'Sun', 'Swift', 'Sword', - 'Thunder', 'Tiger', 'Titan', 'Turbo', 'Venom', 'Viper', 'Void', 'Wave', 'Wild', 'Wind', - 'Wolf', 'Wrath', 'Zen', 'Zero', 'Zeta', 'Zoom', 'Apex', 'Atlas', 'Aura', 'Blade' + 'Ace', + 'Arrow', + 'Blaze', + 'Bolt', + 'Cat', + 'Cloud', + 'Comet', + 'Crow', + 'Crystal', + 'Cyber', + 'Dark', + 'Dawn', + 'Demon', + 'Diamond', + 'Dragon', + 'Dream', + 'Eagle', + 'Echo', + 'Edge', + 'Elite', + 'Ember', + 'Fang', + 'Fire', + 'Flash', + 'Fox', + 'Frost', + 'Ghost', + 'Gold', + 'Hawk', + 'Haze', + 'Hero', + 'Hunt', + 'Ice', + 'Iron', + 'Jade', + 'Jet', + 'King', + 'Knight', + 'Leo', + 'Light', + 'Luna', + 'Lynx', + 'Mist', + 'Moon', + 'Neo', + 'Night', + 'Nova', + 'Omega', + 'Onyx', + 'Phantom', + 'Phoenix', + 'Pixel', + 'Prime', + 'Pulse', + 'Rage', + 'Rain', + 'Raven', + 'Rex', + 'Rise', + 'River', + 'Rock', + 'Ruby', + 'Rush', + 'Sage', + 'Shade', + 'Shadow', + 'Silver', + 'Sky', + 'Snap', + 'Snow', + 'Solar', + 'Soul', + 'Spark', + 'Star', + 'Steel', + 'Storm', + 'Strike', + 'Sun', + 'Swift', + 'Sword', + 'Thunder', + 'Tiger', + 'Titan', + 'Turbo', + 'Venom', + 'Viper', + 'Void', + 'Wave', + 'Wild', + 'Wind', + 'Wolf', + 'Wrath', + 'Zen', + 'Zero', + 'Zeta', + 'Zoom', + 'Apex', + 'Atlas', + 'Aura', + 'Blade' ]; const SUFFIXES = ['', '', '', '_', '-', 'HD', 'osu', 'pp', 'x', 'z', '69', '420', 'YT', 'TTV']; @@ -313,54 +401,210 @@ function generateUsername(index: number): string { } const TEAM_NAMES = [ - 'Team Absolute', 'Velocity Gaming', 'Rising Phoenix', 'Dark Matter', 'Quantum Flux', - 'Storm Chasers', 'Neon Knights', 'Solar Flare', 'Arctic Wolves', 'Thunder Strike', - 'Crimson Tide', 'Iron Fortress', 'Shadow Realm', 'Crystal Edge', 'Blazing Stars', - 'Night Owls', 'Golden Eagles', 'Frost Bite', 'Wild Cards', 'Steel Legion', - 'Moon Riders', 'Star Dust', 'Fire Hawks', 'Ice Breakers', 'Wind Walkers', - 'Dragon Scale', 'Raven Wing', 'Silver Bullet', 'Ghost Protocol', 'Prime Time', - 'Echo Chamber', 'Volt Force', 'Sky Runners', 'Omega Squad', 'Alpha Pack', - 'Zero Gravity', 'Dawn Patrol', 'Pixel Perfect', 'Cyber Monks', 'Neo Tokyo', - 'Mist Walkers', 'Pulse Wave', 'Zenith', 'Apex Legends', 'Void Runners', - 'Soul Reapers', 'Blade Dancers', 'Turbo Charged', 'Snap Dragons', 'Rush Hour', - 'Phantom Force', 'Diamond Dogs', 'Cosmic Rays', 'Hyper Beam', 'Sonic Boom', - 'Prism', 'Eclipse', 'Nebula', 'Supernova', 'Horizon', - 'Catalyst', 'Entropy', 'Vanguard', 'Sentinel', 'Oracle', - 'Maelstrom', 'Tempest', 'Cascade', 'Enigma', 'Paradox', - 'Mirage', 'Nexus', 'Vertex', 'Cipher', 'Stratos', - 'Helix', 'Spectrum', 'Polaris', 'Inferno', 'Tsunami', - 'Rampage', 'Havoc', 'Fury', 'Blitz', 'Reign', - 'Rapture', 'Genesis', 'Onslaught', 'Vendetta', 'Oblivion', - 'Dominion', 'Ascension', 'Redemption', 'Revolution', 'Evolution', - 'Dynasty', 'Empire', 'Republic', 'Alliance', 'Syndicate', - 'Collective', 'Foundation', 'Initiative', 'Consortium', 'Federation' + 'Team Absolute', + 'Velocity Gaming', + 'Rising Phoenix', + 'Dark Matter', + 'Quantum Flux', + 'Storm Chasers', + 'Neon Knights', + 'Solar Flare', + 'Arctic Wolves', + 'Thunder Strike', + 'Crimson Tide', + 'Iron Fortress', + 'Shadow Realm', + 'Crystal Edge', + 'Blazing Stars', + 'Night Owls', + 'Golden Eagles', + 'Frost Bite', + 'Wild Cards', + 'Steel Legion', + 'Moon Riders', + 'Star Dust', + 'Fire Hawks', + 'Ice Breakers', + 'Wind Walkers', + 'Dragon Scale', + 'Raven Wing', + 'Silver Bullet', + 'Ghost Protocol', + 'Prime Time', + 'Echo Chamber', + 'Volt Force', + 'Sky Runners', + 'Omega Squad', + 'Alpha Pack', + 'Zero Gravity', + 'Dawn Patrol', + 'Pixel Perfect', + 'Cyber Monks', + 'Neo Tokyo', + 'Mist Walkers', + 'Pulse Wave', + 'Zenith', + 'Apex Legends', + 'Void Runners', + 'Soul Reapers', + 'Blade Dancers', + 'Turbo Charged', + 'Snap Dragons', + 'Rush Hour', + 'Phantom Force', + 'Diamond Dogs', + 'Cosmic Rays', + 'Hyper Beam', + 'Sonic Boom', + 'Prism', + 'Eclipse', + 'Nebula', + 'Supernova', + 'Horizon', + 'Catalyst', + 'Entropy', + 'Vanguard', + 'Sentinel', + 'Oracle', + 'Maelstrom', + 'Tempest', + 'Cascade', + 'Enigma', + 'Paradox', + 'Mirage', + 'Nexus', + 'Vertex', + 'Cipher', + 'Stratos', + 'Helix', + 'Spectrum', + 'Polaris', + 'Inferno', + 'Tsunami', + 'Rampage', + 'Havoc', + 'Fury', + 'Blitz', + 'Reign', + 'Rapture', + 'Genesis', + 'Onslaught', + 'Vendetta', + 'Oblivion', + 'Dominion', + 'Ascension', + 'Redemption', + 'Revolution', + 'Evolution', + 'Dynasty', + 'Empire', + 'Republic', + 'Alliance', + 'Syndicate', + 'Collective', + 'Foundation', + 'Initiative', + 'Consortium', + 'Federation' ]; // Beatmap metadata templates — used to generate pool slots with varied data const ARTISTS = [ - 'xi', 'DragonForce', 'UNDEAD CORPORATION', 'Imperial Circus Dead Decadence', - 'Camellia', 'FELT', 'Halozy', 'Sound Horizon', 'Reol', 'DECO*27', - 'ClariS', 'LiSA', 'Aimer', 'EGOIST', 'supercell', 'Linked Horizon', - 'Foreground Eclipse', 'BABYMETAL', 'Hana', 'IOSYS', - 'Mafumafu', 'Eve', 'Yorushika', 'YOASOBI', 'Kenshi Yonezu', - 'Aqours', 'Roselia', 'Raise A Suilen', 'Pastel*Palettes', 'Poppin Party' + 'xi', + 'DragonForce', + 'UNDEAD CORPORATION', + 'Imperial Circus Dead Decadence', + 'Camellia', + 'FELT', + 'Halozy', + 'Sound Horizon', + 'Reol', + 'DECO*27', + 'ClariS', + 'LiSA', + 'Aimer', + 'EGOIST', + 'supercell', + 'Linked Horizon', + 'Foreground Eclipse', + 'BABYMETAL', + 'Hana', + 'IOSYS', + 'Mafumafu', + 'Eve', + 'Yorushika', + 'YOASOBI', + 'Kenshi Yonezu', + 'Aqours', + 'Roselia', + 'Raise A Suilen', + 'Pastel*Palettes', + 'Poppin Party' ]; const TITLES = [ - 'FREEDOM DiVE', 'Blue Zenith', 'Uta', 'Image Material', 'Brain Power', - 'The Big Black', 'Airman ga Taosenai', 'Kira Kira Days', 'Senbonzakura', 'Harumachi Clover', - 'Storytellers', 'Highscore', 'Louder than Steel', 'Snow Drive', 'Night of Knights', - 'Yuki no Hana', 'Rainbow Road', 'Galaxy Collapse', 'Last Goodbye', 'Shinbatsu', - 'Crystallize', 'Overkill', 'Plasma Gun', 'Time Freeze', 'Diamond', - 'United', 'Alchemy', 'Scarlet Rose', 'Infinite Dream', 'Lost Umbrella', - 'Through the Fire and Flames', 'Cry Thunder', 'Soldiers of the Wasteland', 'Valley of the Damned', 'Black Fire', - 'Akasha', 'Ethereal', 'Chrono Trigger', 'Solar System', 'Phantom Rider' + 'FREEDOM DiVE', + 'Blue Zenith', + 'Uta', + 'Image Material', + 'Brain Power', + 'The Big Black', + 'Airman ga Taosenai', + 'Kira Kira Days', + 'Senbonzakura', + 'Harumachi Clover', + 'Storytellers', + 'Highscore', + 'Louder than Steel', + 'Snow Drive', + 'Night of Knights', + 'Yuki no Hana', + 'Rainbow Road', + 'Galaxy Collapse', + 'Last Goodbye', + 'Shinbatsu', + 'Crystallize', + 'Overkill', + 'Plasma Gun', + 'Time Freeze', + 'Diamond', + 'United', + 'Alchemy', + 'Scarlet Rose', + 'Infinite Dream', + 'Lost Umbrella', + 'Through the Fire and Flames', + 'Cry Thunder', + 'Soldiers of the Wasteland', + 'Valley of the Damned', + 'Black Fire', + 'Akasha', + 'Ethereal', + 'Chrono Trigger', + 'Solar System', + 'Phantom Rider' ]; const VERSIONS = [ - 'Normal', 'Hard', 'Insane', 'Expert', "Expert+", 'Lunatic', - "Someone's Extra", 'Another', 'Extreme', "Lasse's Insane", - 'Reform', 'Deluge', 'Overdose', 'Rain', 'Platter', - "Monstrata's Expert", "Sotarks' Insane", "fieryrage's Extra", "Kroytz's Insane", - 'Kalibe', 'Cataclysm' + 'Normal', + 'Hard', + 'Insane', + 'Expert', + 'Expert+', + 'Lunatic', + "Someone's Extra", + 'Another', + 'Extreme', + "Lasse's Insane", + 'Reform', + 'Deluge', + 'Overdose', + 'Rain', + 'Platter', + "Monstrata's Expert", + "Sotarks' Insane", + "fieryrage's Extra", + "Kroytz's Insane", + 'Kalibe', + 'Cataclysm' ]; // ── PHASE FUNCTIONS ───────────────────────────────────────────────────── @@ -431,7 +675,13 @@ async function seedUsers(): Promise { await batchInsert(account, accounts); console.log(` Users + accounts: ${USER_COUNT} (${t()})`); - return users.map((u) => ({ id: u.id, name: u.name, email: u.email, role: u.role, createdAt: u.createdAt })); + return users.map((u) => ({ + id: u.id, + name: u.name, + email: u.email, + role: u.role, + createdAt: u.createdAt + })); } async function seedPersonalTeams(users: UserData[]): Promise { @@ -472,7 +722,9 @@ async function seedNonPersonalTeams(users: UserData[]): Promise { for (let i = 0; i < TEAM_COUNT; i++) { teams.push({ - name: TEAM_NAMES[i % TEAM_NAMES.length] + (i >= TEAM_NAMES.length ? ` ${Math.floor(i / TEAM_NAMES.length) + 1}` : ''), + name: + TEAM_NAMES[i % TEAM_NAMES.length] + + (i >= TEAM_NAMES.length ? ` ${Math.floor(i / TEAM_NAMES.length) + 1}` : ''), isPersonal: false, ownerId: users[i].id }); @@ -582,10 +834,15 @@ async function seedMappools(users: UserData[]): Promise { // Generate slots for each pool const SLOT_TEMPLATE = [ - { cat: 'NM', order: 1 }, { cat: 'NM', order: 2 }, { cat: 'NM', order: 3 }, - { cat: 'HD', order: 1 }, { cat: 'HD', order: 2 }, - { cat: 'HR', order: 1 }, { cat: 'HR', order: 2 }, - { cat: 'DT', order: 1 }, { cat: 'DT', order: 2 }, + { cat: 'NM', order: 1 }, + { cat: 'NM', order: 2 }, + { cat: 'NM', order: 3 }, + { cat: 'HD', order: 1 }, + { cat: 'HD', order: 2 }, + { cat: 'HR', order: 1 }, + { cat: 'HR', order: 2 }, + { cat: 'DT', order: 1 }, + { cat: 'DT', order: 2 }, { cat: 'FM', order: 1 }, { cat: 'TB', order: 1 } ]; @@ -833,7 +1090,9 @@ async function seedMatches( } // Strip temp fields for insert - const participantRows = allParticipants.map(({ _tempIdx, _matchDataIdx, _side, ...rest }) => rest); + const participantRows = allParticipants.map( + ({ _tempIdx, _matchDataIdx, _side, ...rest }) => rest + ); const insertedParticipants = await batchInsert(matchParticipant, participantRows); // Build players @@ -851,7 +1110,9 @@ async function seedMatches( }); } - const playerRows = allPlayers.map(({ _participantInsertIdx, _matchDataIdx, _side, ...rest }) => rest); + const playerRows = allPlayers.map( + ({ _participantInsertIdx, _matchDataIdx, _side, ...rest }) => rest + ); const insertedPlayers = await batchInsert(matchParticipantPlayer, playerRows); // Build a lookup: matchDataIdx + side -> participant ID and player ID @@ -948,7 +1209,8 @@ async function seedMatches( winnerParticipantId: gameWinner?.participantId || null, state: gameState, startedAt: gameStarted, - finishedAt: gameState === 'FINISHED' ? new Date(gameStarted.getTime() + randInt(2, 8) * 60000) : null, + finishedAt: + gameState === 'FINISHED' ? new Date(gameStarted.getTime() + randInt(2, 8) * 60000) : null, _scores: [ { playerId: p1.playerId, ...score1 }, { playerId: p2.playerId, ...score2 } @@ -988,8 +1250,7 @@ async function seedMatches( // Update match winners and participant scores for (const [idx, winner] of matchWinners) { const mi = insertedMatches[idx]; - await db - .execute(sql`UPDATE match SET winner_id = ${winner.winnerId} WHERE id = ${mi.id}`); + await db.execute(sql`UPDATE match SET winner_id = ${winner.winnerId} WHERE id = ${mi.id}`); const p1 = participantLookup.get(`${idx}-1`)!; const p2 = participantLookup.get(`${idx}-2`)!; @@ -1023,7 +1284,13 @@ async function seedMatches( async function seedNotifications(users: UserData[]): Promise { const t = timer(); - const types = ['match_created', 'match_finished', 'match_cancelled', 'team_invite', 'match_invite']; + const types = [ + 'match_created', + 'match_finished', + 'match_cancelled', + 'team_invite', + 'match_invite' + ]; const rows: any[] = []; for (const u of users) { diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 4e50112..8e310f6 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -22,10 +22,12 @@ } - { - const target = e.target as HTMLElement; - if (!target.closest('[data-profile-menu]')) closeMenu(); -}} /> + { + const target = e.target as HTMLElement; + if (!target.closest('[data-profile-menu]')) closeMenu(); + }} +/>
{#if user} @@ -37,35 +39,57 @@ aria-haspopup="true" > {#if user.isRootAdmin} - + ROOT {:else if badge} - + {badge.label} {/if} {#if user.image} {user.name} {/if} - - + + {#if menuOpen}
- - - + + + Settings @@ -75,8 +99,18 @@ type="submit" class="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-700 hover:text-red-400" > - - + + Log out diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 75170f0..f833d5e 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -1,10 +1,6 @@
Vash Esports - Vash Esports + Vash Esports {#if user}
@@ -133,9 +223,24 @@ class="relative rounded-md p-1.5 text-text-secondary transition-colors hover:bg-surface-700 hover:text-text-primary" aria-label="Notifications" > - + {#if unreadCount > 0} - {unreadCount > 99 ? '99+' : unreadCount} + {unreadCount > 99 ? '99+' : unreadCount} {/if} @@ -148,18 +253,24 @@ > -
+
- Notifications + Notifications {#if notifications.some((n) => !n.read)} - + {/if}
{#if loadingNotifs}
Loading...
{:else if notifications.length === 0} -
No notifications
+
+ No notifications +
{:else} {#each notifications as n}
@@ -168,22 +279,26 @@ {/if}
-

{n.title}

+

{n.title}

{#if n.message}

{n.message}

{/if} -

{timeAgo(n.createdAt)}

+

+ {timeAgo(n.createdAt)} +

{#if n.type === 'match_invite' && !n.actionedAt && n.referenceId}
+ class="font-600 rounded bg-green-500/20 px-2 py-0.5 text-[10px] text-green-400 transition-colors hover:bg-green-500/30" + >Accept + class="font-600 rounded bg-red-500/20 px-2 py-0.5 text-[10px] text-red-400 transition-colors hover:bg-red-500/30" + >Decline
{/if}
@@ -203,7 +318,7 @@ {#if activeMatch && page.url.pathname !== `/matches/${activeMatch.id}`}
@@ -213,7 +328,9 @@
{:else if queue.status?.inQueue}
-
+
In Queue ({queue.status.queueSize})
@@ -229,26 +346,34 @@ {/if}
-
Go to match → diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts index d84ca21..edf3735 100644 --- a/src/routes/(app)/+page.server.ts +++ b/src/routes/(app)/+page.server.ts @@ -17,23 +17,30 @@ export const load: PageServerLoad = async ({ locals }) => { } // ── LOGGED IN → dashboard data ── - const [recentMatches, liveMatches, [matchCount], [teamCount], [poolCount], [finishedCount], queueStatus] = - await Promise.all([ - db.query.match.findMany({ - with: { participants: { with: { team: true } }, mappool: { with: { slots: true } } }, - orderBy: desc(match.createdAt), - limit: 5 - }), - db.query.match.findMany({ - with: { participants: { with: { team: true } }, mappool: { with: { slots: true } } }, - where: inArray(match.state, ['LOBBY', 'ROLLING', 'PICKING', 'PLAYING']) - }), - db.select({ count: count() }).from(match), - db.select({ count: count() }).from(team), - db.select({ count: count() }).from(mappool), - db.select({ count: count() }).from(match).where(eq(match.state, 'FINISHED')), - getQueueStatus(locals.user.id) - ]); + const [ + recentMatches, + liveMatches, + [matchCount], + [teamCount], + [poolCount], + [finishedCount], + queueStatus + ] = await Promise.all([ + db.query.match.findMany({ + with: { participants: { with: { team: true } }, mappool: { with: { slots: true } } }, + orderBy: desc(match.createdAt), + limit: 5 + }), + db.query.match.findMany({ + with: { participants: { with: { team: true } }, mappool: { with: { slots: true } } }, + where: inArray(match.state, ['LOBBY', 'ROLLING', 'PICKING', 'PLAYING']) + }), + db.select({ count: count() }).from(match), + db.select({ count: count() }).from(team), + db.select({ count: count() }).from(mappool), + db.select({ count: count() }).from(match).where(eq(match.state, 'FINISHED')), + getQueueStatus(locals.user.id) + ]); return { authenticated: true as const, diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index dde8a45..679cd06 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -1,10 +1,6 @@
-

Admin Panel

+

Admin Panel

{data.users.length} registered user{data.users.length !== 1 ? 's' : ''} {#if data.actorIsRootAdmin} - ROOT ADMIN + ROOT ADMIN {/if}

@@ -44,11 +44,11 @@
- Avatar - User - ELO - W/L - Role + Avatar + User + ELO + W/L + Role
{#each data.users as u} @@ -63,7 +63,7 @@ {:else}
{u.name.charAt(0).toUpperCase()}
@@ -72,19 +72,21 @@
-

{u.name}

+

{u.name}

{#if u.isRootAdmin} - ROOT + ROOT {/if}

{u.email}

- {u.elo} + {u.elo} - + {u.wins}W / {u.losses}L @@ -112,7 +114,7 @@ name="role" value={u.role} onchange={(e) => e.currentTarget.form?.requestSubmit()} - class="rounded-md border px-2 py-1 text-xs font-500 {badge.color} cursor-pointer bg-transparent focus:outline-none" + class="font-500 rounded-md border px-2 py-1 text-xs {badge.color} cursor-pointer bg-transparent focus:outline-none" > {#each availableRoles as r}