diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json index 062511c..861b7b1 100644 --- a/drizzle/meta/0010_snapshot.json +++ b/drizzle/meta/0010_snapshot.json @@ -97,12 +97,8 @@ "name": "account_userId_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -110,10 +106,7 @@ "compositePrimaryKeys": { "account_provider_providerAccountId_pk": { "name": "account_provider_providerAccountId_pk", - "columns": [ - "provider", - "providerAccountId" - ] + "columns": ["provider", "providerAccountId"] } }, "uniqueConstraints": {}, @@ -204,12 +197,8 @@ "name": "banned_user_aliases_banned_user_id_banned_users_id_fk", "tableFrom": "banned_user_aliases", "tableTo": "banned_users", - "columnsFrom": [ - "banned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -303,12 +292,8 @@ "name": "banned_user_ids_banned_user_id_banned_users_id_fk", "tableFrom": "banned_user_ids", "tableTo": "banned_users", - "columnsFrom": [ - "banned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -448,12 +433,8 @@ "name": "blog_posts_author_id_user_id_fk", "tableFrom": "blog_posts", "tableTo": "user", - "columnsFrom": [ - "author_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["author_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -463,9 +444,7 @@ "blog_posts_slug_unique": { "name": "blog_posts_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -520,9 +499,7 @@ "mod_branches_name_unique": { "name": "mod_branches_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -967,12 +944,8 @@ "name": "games_log_file_id_log_files_id_fk", "tableFrom": "games", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1068,12 +1041,8 @@ "name": "log_file_connections_log_file_id_log_files_id_fk", "tableFrom": "log_file_connections", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1081,10 +1050,7 @@ "compositePrimaryKeys": { "log_file_connections_log_file_id_connection_id_lower_pk": { "name": "log_file_connections_log_file_id_connection_id_lower_pk", - "columns": [ - "log_file_id", - "connection_id_lower" - ] + "columns": ["log_file_id", "connection_id_lower"] } }, "uniqueConstraints": {}, @@ -1121,12 +1087,8 @@ "name": "log_file_lobby_codes_log_file_id_log_files_id_fk", "tableFrom": "log_file_lobby_codes", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1134,10 +1096,7 @@ "compositePrimaryKeys": { "log_file_lobby_codes_log_file_id_lobby_code_lower_pk": { "name": "log_file_lobby_codes_log_file_id_lobby_code_lower_pk", - "columns": [ - "log_file_id", - "lobby_code_lower" - ] + "columns": ["log_file_id", "lobby_code_lower"] } }, "uniqueConstraints": {}, @@ -1174,12 +1133,8 @@ "name": "log_file_owner_connections_log_file_id_log_files_id_fk", "tableFrom": "log_file_owner_connections", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1187,10 +1142,7 @@ "compositePrimaryKeys": { "log_file_owner_connections_log_file_id_connection_id_lower_pk": { "name": "log_file_owner_connections_log_file_id_connection_id_lower_pk", - "columns": [ - "log_file_id", - "connection_id_lower" - ] + "columns": ["log_file_id", "connection_id_lower"] } }, "uniqueConstraints": {}, @@ -1227,12 +1179,8 @@ "name": "log_file_players_log_file_id_log_files_id_fk", "tableFrom": "log_file_players", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1240,10 +1188,7 @@ "compositePrimaryKeys": { "log_file_players_log_file_id_player_name_lower_pk": { "name": "log_file_players_log_file_id_player_name_lower_pk", - "columns": [ - "log_file_id", - "player_name_lower" - ] + "columns": ["log_file_id", "player_name_lower"] } }, "uniqueConstraints": {}, @@ -1332,12 +1277,8 @@ "name": "log_files_user_id_user_id_fk", "tableFrom": "log_files", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1595,10 +1536,7 @@ "compositePrimaryKeys": { "player_games_player_id_game_num_pk": { "name": "player_games_player_id_game_num_pk", - "columns": [ - "player_id", - "game_num" - ] + "columns": ["player_id", "game_num"] } }, "uniqueConstraints": {}, @@ -1751,12 +1689,8 @@ "name": "mod_release_branch_id_mod_branches_id_fk", "tableFrom": "mod_release", "tableTo": "mod_branches", - "columnsFrom": [ - "branch_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["branch_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1876,12 +1810,8 @@ "name": "season_snapshots_season_id_seasons_id_fk", "tableFrom": "season_snapshots", "tableTo": "seasons", - "columnsFrom": [ - "season_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["season_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1999,12 +1929,8 @@ "name": "session_userId_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2063,9 +1989,7 @@ "transcripts_game_number_unique": { "name": "transcripts_game_number_unique", "nullsNotDistinct": false, - "columns": [ - "game_number" - ] + "columns": ["game_number"] } }, "policies": {}, @@ -2176,10 +2100,7 @@ "compositePrimaryKeys": { "verification_token_identifier_token_pk": { "name": "verification_token_identifier_token_pk", - "columns": [ - "identifier", - "token" - ] + "columns": ["identifier", "token"] } }, "uniqueConstraints": {}, @@ -2199,4 +2120,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/0011_snapshot.json b/drizzle/meta/0011_snapshot.json index 062511c..861b7b1 100644 --- a/drizzle/meta/0011_snapshot.json +++ b/drizzle/meta/0011_snapshot.json @@ -97,12 +97,8 @@ "name": "account_userId_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -110,10 +106,7 @@ "compositePrimaryKeys": { "account_provider_providerAccountId_pk": { "name": "account_provider_providerAccountId_pk", - "columns": [ - "provider", - "providerAccountId" - ] + "columns": ["provider", "providerAccountId"] } }, "uniqueConstraints": {}, @@ -204,12 +197,8 @@ "name": "banned_user_aliases_banned_user_id_banned_users_id_fk", "tableFrom": "banned_user_aliases", "tableTo": "banned_users", - "columnsFrom": [ - "banned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -303,12 +292,8 @@ "name": "banned_user_ids_banned_user_id_banned_users_id_fk", "tableFrom": "banned_user_ids", "tableTo": "banned_users", - "columnsFrom": [ - "banned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["banned_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -448,12 +433,8 @@ "name": "blog_posts_author_id_user_id_fk", "tableFrom": "blog_posts", "tableTo": "user", - "columnsFrom": [ - "author_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["author_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -463,9 +444,7 @@ "blog_posts_slug_unique": { "name": "blog_posts_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -520,9 +499,7 @@ "mod_branches_name_unique": { "name": "mod_branches_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -967,12 +944,8 @@ "name": "games_log_file_id_log_files_id_fk", "tableFrom": "games", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1068,12 +1041,8 @@ "name": "log_file_connections_log_file_id_log_files_id_fk", "tableFrom": "log_file_connections", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1081,10 +1050,7 @@ "compositePrimaryKeys": { "log_file_connections_log_file_id_connection_id_lower_pk": { "name": "log_file_connections_log_file_id_connection_id_lower_pk", - "columns": [ - "log_file_id", - "connection_id_lower" - ] + "columns": ["log_file_id", "connection_id_lower"] } }, "uniqueConstraints": {}, @@ -1121,12 +1087,8 @@ "name": "log_file_lobby_codes_log_file_id_log_files_id_fk", "tableFrom": "log_file_lobby_codes", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1134,10 +1096,7 @@ "compositePrimaryKeys": { "log_file_lobby_codes_log_file_id_lobby_code_lower_pk": { "name": "log_file_lobby_codes_log_file_id_lobby_code_lower_pk", - "columns": [ - "log_file_id", - "lobby_code_lower" - ] + "columns": ["log_file_id", "lobby_code_lower"] } }, "uniqueConstraints": {}, @@ -1174,12 +1133,8 @@ "name": "log_file_owner_connections_log_file_id_log_files_id_fk", "tableFrom": "log_file_owner_connections", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1187,10 +1142,7 @@ "compositePrimaryKeys": { "log_file_owner_connections_log_file_id_connection_id_lower_pk": { "name": "log_file_owner_connections_log_file_id_connection_id_lower_pk", - "columns": [ - "log_file_id", - "connection_id_lower" - ] + "columns": ["log_file_id", "connection_id_lower"] } }, "uniqueConstraints": {}, @@ -1227,12 +1179,8 @@ "name": "log_file_players_log_file_id_log_files_id_fk", "tableFrom": "log_file_players", "tableTo": "log_files", - "columnsFrom": [ - "log_file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["log_file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1240,10 +1188,7 @@ "compositePrimaryKeys": { "log_file_players_log_file_id_player_name_lower_pk": { "name": "log_file_players_log_file_id_player_name_lower_pk", - "columns": [ - "log_file_id", - "player_name_lower" - ] + "columns": ["log_file_id", "player_name_lower"] } }, "uniqueConstraints": {}, @@ -1332,12 +1277,8 @@ "name": "log_files_user_id_user_id_fk", "tableFrom": "log_files", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1595,10 +1536,7 @@ "compositePrimaryKeys": { "player_games_player_id_game_num_pk": { "name": "player_games_player_id_game_num_pk", - "columns": [ - "player_id", - "game_num" - ] + "columns": ["player_id", "game_num"] } }, "uniqueConstraints": {}, @@ -1751,12 +1689,8 @@ "name": "mod_release_branch_id_mod_branches_id_fk", "tableFrom": "mod_release", "tableTo": "mod_branches", - "columnsFrom": [ - "branch_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["branch_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1876,12 +1810,8 @@ "name": "season_snapshots_season_id_seasons_id_fk", "tableFrom": "season_snapshots", "tableTo": "seasons", - "columnsFrom": [ - "season_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["season_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1999,12 +1929,8 @@ "name": "session_userId_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2063,9 +1989,7 @@ "transcripts_game_number_unique": { "name": "transcripts_game_number_unique", "nullsNotDistinct": false, - "columns": [ - "game_number" - ] + "columns": ["game_number"] } }, "policies": {}, @@ -2176,10 +2100,7 @@ "compositePrimaryKeys": { "verification_token_identifier_token_pk": { "name": "verification_token_identifier_token_pk", - "columns": [ - "identifier", - "token" - ] + "columns": ["identifier", "token"] } }, "uniqueConstraints": {}, @@ -2199,4 +2120,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/public/bounties/6_pack.png b/public/bounties/6_pack.png new file mode 100644 index 0000000..0f35314 Binary files /dev/null and b/public/bounties/6_pack.png differ diff --git a/public/bounties/9_hole_hitter.png b/public/bounties/9_hole_hitter.png new file mode 100644 index 0000000..1e6e16f Binary files /dev/null and b/public/bounties/9_hole_hitter.png differ diff --git a/public/bounties/aarons_best_build_bounty.png b/public/bounties/aarons_best_build_bounty.png new file mode 100644 index 0000000..1db0625 Binary files /dev/null and b/public/bounties/aarons_best_build_bounty.png differ diff --git a/public/bounties/autoclicker.png b/public/bounties/autoclicker.png new file mode 100644 index 0000000..7c26afe Binary files /dev/null and b/public/bounties/autoclicker.png differ diff --git a/public/bounties/buffet.png b/public/bounties/buffet.png new file mode 100644 index 0000000..65ff456 Binary files /dev/null and b/public/bounties/buffet.png differ diff --git a/public/bounties/bulk.png b/public/bounties/bulk.png new file mode 100644 index 0000000..97f751b Binary files /dev/null and b/public/bounties/bulk.png differ diff --git a/public/bounties/doink.png b/public/bounties/doink.png new file mode 100644 index 0000000..cb10e9b Binary files /dev/null and b/public/bounties/doink.png differ diff --git a/public/bounties/empty_hourglass.png b/public/bounties/empty_hourglass.png new file mode 100644 index 0000000..36d91ad Binary files /dev/null and b/public/bounties/empty_hourglass.png differ diff --git a/public/bounties/everest.png b/public/bounties/everest.png new file mode 100644 index 0000000..897fac3 Binary files /dev/null and b/public/bounties/everest.png differ diff --git a/public/bounties/fourth_quarter.png b/public/bounties/fourth_quarter.png new file mode 100644 index 0000000..3cd792e Binary files /dev/null and b/public/bounties/fourth_quarter.png differ diff --git a/public/bounties/from_scratch.png b/public/bounties/from_scratch.png new file mode 100644 index 0000000..c1cd97d Binary files /dev/null and b/public/bounties/from_scratch.png differ diff --git a/public/bounties/get_to_see_more_of_your_deck.png b/public/bounties/get_to_see_more_of_your_deck.png new file mode 100644 index 0000000..8e65580 Binary files /dev/null and b/public/bounties/get_to_see_more_of_your_deck.png differ diff --git a/public/bounties/good_ol_days.png b/public/bounties/good_ol_days.png new file mode 100644 index 0000000..63deb98 Binary files /dev/null and b/public/bounties/good_ol_days.png differ diff --git a/public/bounties/high_pitched.png b/public/bounties/high_pitched.png new file mode 100644 index 0000000..5bd88e7 Binary files /dev/null and b/public/bounties/high_pitched.png differ diff --git a/public/bounties/hydras_space_joker_challenge.png b/public/bounties/hydras_space_joker_challenge.png new file mode 100644 index 0000000..bb24940 Binary files /dev/null and b/public/bounties/hydras_space_joker_challenge.png differ diff --git a/public/bounties/hydras_space_joker_challenge_alt.png b/public/bounties/hydras_space_joker_challenge_alt.png new file mode 100644 index 0000000..a8d17bb Binary files /dev/null and b/public/bounties/hydras_space_joker_challenge_alt.png differ diff --git a/public/bounties/induction.png b/public/bounties/induction.png new file mode 100644 index 0000000..0f6d261 Binary files /dev/null and b/public/bounties/induction.png differ diff --git a/public/bounties/irs.png b/public/bounties/irs.png new file mode 100644 index 0000000..dff3eda Binary files /dev/null and b/public/bounties/irs.png differ diff --git a/public/bounties/is_that_a_dagger_in_your_pants_or_are_you_just_happy_to_see_me.png b/public/bounties/is_that_a_dagger_in_your_pants_or_are_you_just_happy_to_see_me.png new file mode 100644 index 0000000..5907f68 Binary files /dev/null and b/public/bounties/is_that_a_dagger_in_your_pants_or_are_you_just_happy_to_see_me.png differ diff --git a/public/bounties/maybe_top_250.png b/public/bounties/maybe_top_250.png new file mode 100644 index 0000000..2d43a81 Binary files /dev/null and b/public/bounties/maybe_top_250.png differ diff --git a/public/bounties/money_doesnt_buy_happi_mmr.png b/public/bounties/money_doesnt_buy_happi_mmr.png new file mode 100644 index 0000000..70c1d87 Binary files /dev/null and b/public/bounties/money_doesnt_buy_happi_mmr.png differ diff --git a/public/bounties/placeholder.png b/public/bounties/placeholder.png new file mode 100644 index 0000000..388d7df Binary files /dev/null and b/public/bounties/placeholder.png differ diff --git a/public/bounties/polycule.png b/public/bounties/polycule.png new file mode 100644 index 0000000..7169194 Binary files /dev/null and b/public/bounties/polycule.png differ diff --git a/public/bounties/showman_cycle.png b/public/bounties/showman_cycle.png new file mode 100644 index 0000000..3a325a2 Binary files /dev/null and b/public/bounties/showman_cycle.png differ diff --git a/public/bounties/single_player.png b/public/bounties/single_player.png new file mode 100644 index 0000000..55c7cae Binary files /dev/null and b/public/bounties/single_player.png differ diff --git a/public/bounties/skipping_stones.png b/public/bounties/skipping_stones.png new file mode 100644 index 0000000..79f5a4f Binary files /dev/null and b/public/bounties/skipping_stones.png differ diff --git a/public/bounties/studious.png b/public/bounties/studious.png new file mode 100644 index 0000000..e43e537 Binary files /dev/null and b/public/bounties/studious.png differ diff --git a/public/bounties/they_made_me_change_the_name_of_this_one.png b/public/bounties/they_made_me_change_the_name_of_this_one.png new file mode 100644 index 0000000..b9f858a Binary files /dev/null and b/public/bounties/they_made_me_change_the_name_of_this_one.png differ diff --git a/public/bounties/yard_sale.png b/public/bounties/yard_sale.png new file mode 100644 index 0000000..3dc0789 Binary files /dev/null and b/public/bounties/yard_sale.png differ diff --git a/scripts/rename-season-leaderboard-player.ts b/scripts/rename-season-leaderboard-player.ts index f1f9394..6f5e14a 100644 --- a/scripts/rename-season-leaderboard-player.ts +++ b/scripts/rename-season-leaderboard-player.ts @@ -342,7 +342,9 @@ async function main() { await redis.del(getSeasonQueuesCacheKey(args.seasonId)) console.log(`uploaded=${nextObjectKey}`) - console.log(`cleared=${getSeasonLeaderboardKey(args.seasonId, snapshot.queueId)}`) + console.log( + `cleared=${getSeasonLeaderboardKey(args.seasonId, snapshot.queueId)}` + ) } main() diff --git a/src/app/(home)/admin/lobby-codes/lobby-codes-client.tsx b/src/app/(home)/admin/lobby-codes/lobby-codes-client.tsx index 518d83e..577e0bd 100644 --- a/src/app/(home)/admin/lobby-codes/lobby-codes-client.tsx +++ b/src/app/(home)/admin/lobby-codes/lobby-codes-client.tsx @@ -85,10 +85,7 @@ export function LobbyCodesClient({ - + Loading... diff --git a/src/app/(home)/admin/lobby-codes/page.tsx b/src/app/(home)/admin/lobby-codes/page.tsx index fb08c8c..2df9b84 100644 --- a/src/app/(home)/admin/lobby-codes/page.tsx +++ b/src/app/(home)/admin/lobby-codes/page.tsx @@ -18,10 +18,7 @@ export default async function LobbyCodesPage({ searchParams: Promise> }) { const session = await auth() - const canSearchLobbyCodes = hasPermission( - session?.user, - 'transcripts.search' - ) + const canSearchLobbyCodes = hasPermission(session?.user, 'transcripts.search') if (!canSearchLobbyCodes) { return ( @@ -34,8 +31,7 @@ export default async function LobbyCodesPage({ } const params = await searchParams - const search = - typeof params.search === 'string' ? params.search.trim() : '' + const search = typeof params.search === 'string' ? params.search.trim() : '' if (search.length > 0) { await api.history.searchTranscriptLobbyCodes.prefetch({ diff --git a/src/app/(home)/admin/permissions/permissions-client.tsx b/src/app/(home)/admin/permissions/permissions-client.tsx index 5cb8de5..6858afe 100644 --- a/src/app/(home)/admin/permissions/permissions-client.tsx +++ b/src/app/(home)/admin/permissions/permissions-client.tsx @@ -334,7 +334,7 @@ export function PermissionsClient() { -
+
{PERMISSION_GROUPS.map((group) => { const groupKeys = group.permissions.map((p) => p.key) const checkedCount = groupKeys.filter((k) => @@ -351,24 +351,32 @@ export function PermissionsClient() { > {group.title}
{group.permissions.map((permission) => { - const checked = draftPermissions.includes(permission.key) + const checked = draftPermissions.includes( + permission.key + ) const checkboxId = `perm-${permission.key}` const descId = `desc-${permission.key}` @@ -377,7 +385,7 @@ export function PermissionsClient() { key={permission.key} htmlFor={checkboxId} className={cn( - 'flex cursor-pointer items-start gap-3 rounded-md border p-2 transition-colors select-none', + 'flex cursor-pointer select-none items-start gap-3 rounded-md border p-2 transition-colors', checked ? 'border-primary/30 bg-primary/5' : 'border-transparent hover:border-border hover:bg-muted/30' @@ -413,7 +421,7 @@ export function PermissionsClient() { ) })} -
+
diff --git a/src/app/(home)/bounties/[id]/bounty-completions.tsx b/src/app/(home)/bounties/[id]/bounty-completions.tsx new file mode 100644 index 0000000..f64cef3 --- /dev/null +++ b/src/app/(home)/bounties/[id]/bounty-completions.tsx @@ -0,0 +1,154 @@ +'use client' + +import { format } from 'date-fns' +import Image from 'next/image' +import Link from 'next/link' +import { useState } from 'react' +import { cn } from '@/lib/utils' +import { api, type RouterOutputs } from '@/trpc/react' + +type BountyCompletion = + RouterOutputs['bounties']['get_bounty_completions']['completions'][number] + +function bountyNameToIconPath(bountyName: string): string { + const slug = bountyName + .toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, '') + return `/bounties/${slug}.png` +} + +function CompletionRow({ + completion, + index, +}: { + completion: BountyCompletion + index: number +}) { + return ( + + + {index + 1} + + + {completion.display_name} + + {completion.is_first && ( + + First + + )} + + {format(new Date(completion.completed_at), 'MMM d, yyyy')} + + + ) +} + +export function BountyCompletions({ bountyName }: { bountyName: string }) { + const [imgError, setImgError] = useState(false) + const { data, isLoading } = api.bounties.get_bounty_completions.useQuery({ + bounty_name: bountyName, + }) + + if (isLoading) { + return ( +
+ Loading... +
+ ) + } + + if (!data) { + return ( +
+ Bounty not found +
+ ) + } + + const { bounty, completions } = data + const iconPath = bountyNameToIconPath(bounty.bounty_name) + const firstCompletion = completions.find((c) => c.is_first) + const sorted = [...completions].sort( + (a, b) => + new Date(a.completed_at).getTime() - new Date(b.completed_at).getTime() + ) + + return ( +
+ {/* Bounty header */} +
+
+ {bounty.bounty_name} setImgError(true)} + /> +
+
+

{bounty.bounty_name}

+

+ {bounty.description} +

+

+ + {completions.length} + {' '} + {completions.length === 1 ? 'completion' : 'completions'} +

+
+
+ + {/* First completion callout */} + {firstCompletion && ( +
+

+ First Completion +

+ + + {firstCompletion.display_name} + + + {format(new Date(firstCompletion.completed_at), 'MMM d, yyyy')} + + +
+ )} + + {/* All completions */} + {sorted.length > 0 && ( +
+
+

All Completions

+
+
+ {sorted.map((completion, index) => ( + + ))} +
+
+ )} +
+ ) +} diff --git a/src/app/(home)/bounties/[id]/page.tsx b/src/app/(home)/bounties/[id]/page.tsx new file mode 100644 index 0000000..02fd661 --- /dev/null +++ b/src/app/(home)/bounties/[id]/page.tsx @@ -0,0 +1,26 @@ +import { Suspense } from 'react' +import { api, HydrateClient } from '@/trpc/server' +import { BountyCompletions } from './bounty-completions' + +type Props = { + params: Promise<{ id: string }> +} + +export default async function BountyPage({ params }: Props) { + const { id } = await params + const bountyName = decodeURIComponent(id) + + await api.bounties.get_bounty_completions.prefetch({ + bounty_name: bountyName, + }) + + return ( + + +
+ +
+
+
+ ) +} diff --git a/src/app/(home)/leaderboards/page.tsx b/src/app/(home)/leaderboards/page.tsx index e681af3..f882c0c 100644 --- a/src/app/(home)/leaderboards/page.tsx +++ b/src/app/(home)/leaderboards/page.tsx @@ -36,11 +36,15 @@ export default async function Home({ }) { const params = await searchParams - const activeSeasonNumber = await getActiveSeasonNumber() - const activeSeason = getSeasonKey(activeSeasonNumber) const seasons = await api.seasons.list() const seasonIds = new Set(seasons.map((season) => season.id)) + const activeSeasonNumber = + seasons.find((s) => s.isActive)?.id ?? + seasons.at(-1)?.id ?? + (await getActiveSeasonNumber()) + const activeSeason = getSeasonKey(activeSeasonNumber) + const requestedSeason = SeasonSchema.safeParse(params.season).success && params.season !== undefined && diff --git a/src/app/(home)/log-parser/page.tsx b/src/app/(home)/log-parser/page.tsx index e0a673e..811c102 100644 --- a/src/app/(home)/log-parser/page.tsx +++ b/src/app/(home)/log-parser/page.tsx @@ -775,7 +775,8 @@ function FinalJokerList({ // Main component export default function LogParser() { const formatter = useFormatter() - const resolveCocktailDecksMutation = api.logs.resolveCocktailDecks.useMutation() + const resolveCocktailDecksMutation = + api.logs.resolveCocktailDecks.useMutation() const searchParams = useSearchParams() const { data: session } = useSession() const [parsedGames, setParsedGames] = useState([]) diff --git a/src/app/(home)/players/[id]/page.tsx b/src/app/(home)/players/[id]/page.tsx index 424993b..c618209 100644 --- a/src/app/(home)/players/[id]/page.tsx +++ b/src/app/(home)/players/[id]/page.tsx @@ -66,6 +66,7 @@ export default async function PlayerPage({ params, searchParams }: Props) { if (id) { await Promise.all([ + api.bounties.get_user_bounties.prefetch({ user_id: id }), api.history.user_games.prefetch({ user_id: id }), api.history.user_games_page.prefetch({ user_id: id, diff --git a/src/app/(home)/players/[id]/user.tsx b/src/app/(home)/players/[id]/user.tsx index 3551b0f..9204ab1 100644 --- a/src/app/(home)/players/[id]/user.tsx +++ b/src/app/(home)/players/[id]/user.tsx @@ -14,6 +14,7 @@ import Image from 'next/image' import { useParams } from 'next/navigation' import { useMemo, useState } from 'react' import { isNonNullish } from 'remeda' +import { BountyCompletions } from '@/app/(home)/bounties/[id]/bounty-completions' import { DeckImage, DeckStakeStatsChart, @@ -27,6 +28,7 @@ import { TimeZoneProvider } from '@/components/timezone-provider' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' import { Label } from '@/components/ui/label' import { Tooltip, @@ -48,6 +50,7 @@ import { } from '@/components/ui/select' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { cn } from '@/lib/utils' +import type { UserBounty } from '@/server/services/botlatro.service' import { LEGACY_QUEUE_ID, OLD_RANKED_CHANNEL, @@ -129,6 +132,7 @@ const TAB_OPTIONS = [ { value: 'deck-stake-stats', label: 'Decks / Stakes' }, { value: 'mmr-trends', label: 'MMR Trends' }, { value: 'winrate-trends', label: 'Winrate Trends' }, + { value: 'bounties', label: 'Bounties' }, ] as const function UserInfoComponent() { @@ -149,6 +153,10 @@ function UserInfoComponent() { user_id: id, }) + const { data: bounties = [] } = api.bounties.get_user_bounties.useQuery({ + user_id: id, + }) + const [rankedLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery({ channel_id: rankedChannelId, season, @@ -786,6 +794,9 @@ function UserInfoComponent() { + + +
@@ -914,3 +925,111 @@ function QueueCard({ ) } + +/* ── Bounties Section ── */ + +function bountyNameToIconPath(bountyName: string): string { + console.log(bountyName) + const slug = bountyName + .toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, '') + return `/bounties/${slug}.png` +} + +function BountyIcon({ bounty }: { bounty: UserBounty }) { + const [open, setOpen] = useState(false) + const [imgError, setImgError] = useState(false) + return ( + <> + + + + + + +

{bounty.bounty_name}

+

{bounty.description}

+
+
+
+ + + {bounty.bounty_name} + + + + + ) +} + +function BountiesSection({ bounties }: { bounties: UserBounty[] }) { + if (bounties.length === 0) { + return ( +
+ + No bounties earned yet + +
+ ) + } + + const firstCompletions = bounties.filter((b) => b.is_first) + const regularCompletions = bounties.filter((b) => !b.is_first) + + return ( +
+ {firstCompletions.length > 0 && ( +
+

+ First Completions +

+
+ {firstCompletions.map((bounty) => ( + + ))} +
+
+ )} + {regularCompletions.length > 0 && ( +
+

Completions

+
+ {regularCompletions.map((bounty) => ( + + ))} +
+
+ )} +
+ ) +} diff --git a/src/app/(home)/stats/_components/deck-popularity-chart.tsx b/src/app/(home)/stats/_components/deck-popularity-chart.tsx index 47e9456..5c63862 100644 --- a/src/app/(home)/stats/_components/deck-popularity-chart.tsx +++ b/src/app/(home)/stats/_components/deck-popularity-chart.tsx @@ -4,8 +4,6 @@ import { format } from 'date-fns' import { BarChart3, CalendarIcon, PieChartIcon } from 'lucide-react' import { parseAsString, useQueryStates } from 'nuqs' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Label } from '@/components/ui/label' -import { Switch } from '@/components/ui/switch' import { Bar, BarChart, @@ -27,6 +25,7 @@ import { ChartTooltip, ChartTooltipContent, } from '@/components/ui/chart' +import { Label } from '@/components/ui/label' import { Popover, PopoverContent, @@ -39,6 +38,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { cn } from '@/lib/utils' import { @@ -271,7 +271,7 @@ export function DeckPopularityChart({ /> diff --git a/src/app/(home)/stats/_components/stats-tabs.tsx b/src/app/(home)/stats/_components/stats-tabs.tsx index 22254a2..0d4abed 100644 --- a/src/app/(home)/stats/_components/stats-tabs.tsx +++ b/src/app/(home)/stats/_components/stats-tabs.tsx @@ -32,7 +32,9 @@ const TABS = STAT_TABS.map((value) => ({ ? 'Season Overview' : value === 'live-matches' ? 'Live Matches' - : 'Game Activity', + : value === 'bounties' + ? 'Bounties' + : 'Game Activity', })) type StatsTabsProps = { diff --git a/src/app/(home)/stats/search-params.constants.ts b/src/app/(home)/stats/search-params.constants.ts index 8479fd4..e95103d 100644 --- a/src/app/(home)/stats/search-params.constants.ts +++ b/src/app/(home)/stats/search-params.constants.ts @@ -13,6 +13,7 @@ export const STAT_TABS = [ 'season-overview', 'game-activity', 'live-matches', + 'bounties', ] as const export const STATS_FILTER_MODES = ['season', 'dateRange'] as const diff --git a/src/app/api/logs/upload/route.ts b/src/app/api/logs/upload/route.ts index 350c147..e2da333 100644 --- a/src/app/api/logs/upload/route.ts +++ b/src/app/api/logs/upload/route.ts @@ -684,7 +684,11 @@ export async function PUT(req: NextRequest) { botlatro_service .sendWarning({ title: `Warning: banned user match detected in uploaded log #${logFileId}`, - lines: formatBannedUserWarningLines(bannedMatches, logUrl, lobbyCodes), + lines: formatBannedUserWarningLines( + bannedMatches, + logUrl, + lobbyCodes + ), }) .catch((error) => { console.error( diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 50f6c04..706a09e 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,5 +1,6 @@ import { bannedUsersRouter } from '@/server/api/routers/banned-users' import { blogRouter } from '@/server/api/routers/blog' +import { bountiesRouter } from '@/server/api/routers/bounties' import { branchesRouter } from '@/server/api/routers/branches' import { history_router } from '@/server/api/routers/history' import { leaderboard_router } from '@/server/api/routers/leaderboard' @@ -7,8 +8,8 @@ import { logsRouter } from '@/server/api/routers/logs' import { moderationRouter } from '@/server/api/routers/moderation' import { playerStateRouter } from '@/server/api/routers/player-state' import { playersRouter } from '@/server/api/routers/players' -import { queuesRouter } from '@/server/api/routers/queues' import { profileRouter } from '@/server/api/routers/profile' +import { queuesRouter } from '@/server/api/routers/queues' import { releasesRouter } from '@/server/api/routers/releases' import { seasonsRouter } from '@/server/api/routers/seasons' import { stats_router } from '@/server/api/routers/stats' @@ -23,6 +24,7 @@ import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc' export const appRouter = createTRPCRouter({ bannedUsers: bannedUsersRouter, blog: blogRouter, + bounties: bountiesRouter, branches: branchesRouter, history: history_router, players: playersRouter, diff --git a/src/server/api/routers/bounties.ts b/src/server/api/routers/bounties.ts new file mode 100644 index 0000000..2a7c9dc --- /dev/null +++ b/src/server/api/routers/bounties.ts @@ -0,0 +1,24 @@ +import { z } from 'zod' +import { createTRPCRouter, publicProcedure } from '@/server/api/trpc' +import { botlatro_service } from '@/server/services/botlatro.service' +import { DISCORD_SNOWFLAKE_REGEX } from '@/shared/discord' + +export const bountiesRouter = createTRPCRouter({ + get_user_bounties: publicProcedure + .input( + z.object({ + user_id: z + .string() + .regex(DISCORD_SNOWFLAKE_REGEX, 'Invalid Discord user ID'), + }) + ) + .query(async ({ input }) => { + return botlatro_service.get_user_bounties(input.user_id) + }), + + get_bounty_completions: publicProcedure + .input(z.object({ bounty_name: z.string().min(1) })) + .query(async ({ input }) => { + return botlatro_service.get_bounty_completions(input.bounty_name) + }), +}) diff --git a/src/server/api/routers/history.ts b/src/server/api/routers/history.ts index dc7184d..afd31db 100644 --- a/src/server/api/routers/history.ts +++ b/src/server/api/routers/history.ts @@ -220,20 +220,20 @@ export const history_router = createTRPCRouter({ input.sortBy === 'gameId' ? player_games.gameId : input.sortBy === 'opponentName' - ? player_games.opponentName - : input.sortBy === 'gameType' - ? player_games.gameType - : input.sortBy === 'deck' - ? player_games.deck - : input.sortBy === 'stake' - ? player_games.stake - : input.sortBy === 'opponentMmr' - ? player_games.opponentMmr - : input.sortBy === 'playerMmr' - ? player_games.playerMmr - : input.sortBy === 'mmrChange' - ? player_games.mmrChange - : player_games.gameTime + ? player_games.opponentName + : input.sortBy === 'gameType' + ? player_games.gameType + : input.sortBy === 'deck' + ? player_games.deck + : input.sortBy === 'stake' + ? player_games.stake + : input.sortBy === 'opponentMmr' + ? player_games.opponentMmr + : input.sortBy === 'playerMmr' + ? player_games.playerMmr + : input.sortBy === 'mmrChange' + ? player_games.mmrChange + : player_games.gameTime const where = and( eq(player_games.playerId, input.user_id), diff --git a/src/server/api/routers/logs.ts b/src/server/api/routers/logs.ts index fb933de..9d67504 100644 --- a/src/server/api/routers/logs.ts +++ b/src/server/api/routers/logs.ts @@ -18,11 +18,9 @@ async function fetchCocktailDecks(seed: string, config: string) { return null } - const payload = (await response.json().catch(() => null)) as - | { - decks?: unknown - } - | null + const payload = (await response.json().catch(() => null)) as { + decks?: unknown + } | null if (!Array.isArray(payload?.decks)) { return null diff --git a/src/server/services/botlatro.service.ts b/src/server/services/botlatro.service.ts index 9af873e..48a3538 100644 --- a/src/server/services/botlatro.service.ts +++ b/src/server/services/botlatro.service.ts @@ -7,11 +7,14 @@ import { redis } from '../redis' const BOTLATRO_URL = 'http://balatro.virtualized.dev:4931/' const TRANSCRIPT_CACHE_TTL_SECONDS = 60 * 60 * 24 * 7 const GUILD_MEMBER_SEARCH_CACHE_TTL_SECONDS = 60 * 60 * 24 +const GUILD_MEMBER_CACHE_TTL_SECONDS = 60 * 60 * 24 export const TRANSCRIPT_CACHE_KEY = (gameNumber: number) => `transcript:${gameNumber}` export const GUILD_MEMBER_SEARCH_CACHE_KEY = (query: string) => `discord:guild-member-search:${query.toLowerCase()}` +export const GUILD_MEMBER_CACHE_KEY = (user_id: string) => + `discord:guild-member:${user_id}` async function botlatroAuthedRequest( path: string, @@ -76,9 +79,20 @@ type TranscriptLobbyCodeSearchResponse = { export const botlatro_service = { getUser: async (user_id: string): Promise => { - return botlatroAuthedRequest( + const cacheKey = GUILD_MEMBER_CACHE_KEY(user_id) + const cached = await redis.get(cacheKey) + if (cached) { + return JSON.parse(cached) as GuildMemberUser + } + const member = await botlatroAuthedRequest( `api/users/${encodeURIComponent(user_id)}` ) + await redis.setEx( + cacheKey, + GUILD_MEMBER_CACHE_TTL_SECONDS, + JSON.stringify(member) + ) + return member }, getQueueSettings: async (): Promise => { @@ -421,6 +435,21 @@ export const botlatro_service = { body: JSON.stringify(input), }) }, + + get_user_bounties: async (user_id: string): Promise => { + const result = await botlatroAuthedRequest<{ bounties: UserBounty[] }>( + `api/bounties/user/${encodeURIComponent(user_id)}` + ) + return result.bounties + }, + + get_bounty_completions: async ( + bounty_name: string + ): Promise => { + return botlatroAuthedRequest( + `api/bounties/${encodeURIComponent(bounty_name)}/completions` + ) + }, } export type QueueSettings = { @@ -631,3 +660,31 @@ export type WarningInput = { export type MonitoringSuccess = { success: true } + +export type UserBounty = { + id: number + bounty_id: number + user_id: string + is_first: boolean + completed_at: string + bounty_name: string + description: string +} + +export type BountyCompletion = { + id: number + bounty_id: number + user_id: string + display_name: string + is_first: boolean + completed_at: string +} + +export type BountyCompletionsResponse = { + bounty: { + id: number + bounty_name: string + description: string + } + completions: BountyCompletion[] +}