From fb044c1d696513d49cab4112c84846fd0d0bfa41 Mon Sep 17 00:00:00 2001 From: Sarah Schulte Date: Sat, 14 Mar 2026 18:03:06 -0700 Subject: [PATCH 01/10] proxy to prod api --- next.config.ts | 11 +++++++++++ src/requests/config.ts | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/next.config.ts b/next.config.ts index 3dd3515..6c9c50d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -45,6 +45,17 @@ const nextConfig: NextConfig = { unoptimized: true, }, transpilePackages: ["bioloom-ui", "bioloom-miniplayer"], + async rewrites() { + if (process.env.NEXT_PUBLIC_MODE === "DEV") { + return [ + { + source: "/api/v1/:path*", + destination: "https://d2jam.com/api/v1/:path*", + }, + ]; + } + return []; + }, }; // -- Apply and export -- diff --git a/src/requests/config.ts b/src/requests/config.ts index 14f5258..15c1781 100644 --- a/src/requests/config.ts +++ b/src/requests/config.ts @@ -1,4 +1,6 @@ export const BASE_URL = process.env.NEXT_PUBLIC_MODE === "PROD" ? "https://d2jam.com/api/v1" - : "http://localhost:3005/api/v1"; + : process.env.NEXT_PUBLIC_API === "local" + ? "http://localhost:3005/api/v1" + : "http://localhost:3000/api/v1"; From b38b027c42ce05bbf0b0dc45aed49067303777aa Mon Sep 17 00:00:00 2001 From: Sarah Schulte Date: Sun, 15 Mar 2026 12:26:46 -0700 Subject: [PATCH 02/10] refactor to use tanstack query --- package-lock.json | 78 +++++- package.json | 3 +- src/app/(main)/SplashDate.tsx | 16 +- src/app/(main)/about/page.tsx | 20 +- src/app/(main)/e/[slug]/page.tsx | 17 +- src/app/(main)/inbox/page.tsx | 38 +-- src/app/providers.tsx | 72 +++--- src/components/admin/AdminImages.tsx | 69 ++--- src/components/admin/AdminJams.tsx | 53 +--- .../admin/AdminThemeEliminationResults.tsx | 36 +-- .../admin/AdminThemeSuggestions.tsx | 35 +-- .../admin/AdminThemeVotingResults.tsx | 36 +-- src/components/events/index.tsx | 50 +--- src/components/games/index.tsx | 243 +++++++----------- src/components/navbar/mobilebar/Mobilebar.tsx | 31 +-- src/components/navbar/pcbar/index.tsx | 86 ++----- src/components/posts/index.tsx | 130 ++++------ src/components/sidebar/SidebarEvents.tsx | 32 +-- src/components/sidebar/SidebarGames.tsx | 50 ++-- src/components/sidebar/SidebarMusic.tsx | 49 +--- src/components/sidebar/SidebarStreams.tsx | 27 +- src/components/timers/index.tsx | 22 +- src/hooks/queries/helpers.ts | 19 ++ src/hooks/queries/index.ts | 11 + src/hooks/queries/queryKeys.ts | 115 +++++++++ src/hooks/queries/useEmojiQueries.ts | 35 +++ src/hooks/queries/useEventQueries.ts | 31 +++ src/hooks/queries/useGameQueries.ts | 109 ++++++++ src/hooks/queries/useJamQueries.ts | 39 +++ src/hooks/queries/useMiscQueries.ts | 87 +++++++ src/hooks/queries/usePostQueries.ts | 39 +++ src/hooks/queries/useTeamQueries.ts | 44 ++++ src/hooks/queries/useThemeQueries.ts | 59 +++++ src/hooks/queries/useUserQueries.ts | 48 ++++ src/hooks/useJam.ts | 23 +- src/providers/EmojiProvider.tsx | 45 +--- src/providers/SiteThemeProvider.tsx | 29 +-- src/types/UserType.ts | 3 + 38 files changed, 1058 insertions(+), 871 deletions(-) create mode 100644 src/hooks/queries/helpers.ts create mode 100644 src/hooks/queries/index.ts create mode 100644 src/hooks/queries/queryKeys.ts create mode 100644 src/hooks/queries/useEmojiQueries.ts create mode 100644 src/hooks/queries/useEventQueries.ts create mode 100644 src/hooks/queries/useGameQueries.ts create mode 100644 src/hooks/queries/useJamQueries.ts create mode 100644 src/hooks/queries/useMiscQueries.ts create mode 100644 src/hooks/queries/usePostQueries.ts create mode 100644 src/hooks/queries/useTeamQueries.ts create mode 100644 src/hooks/queries/useThemeQueries.ts create mode 100644 src/hooks/queries/useUserQueries.ts diff --git a/package-lock.json b/package-lock.json index 908274e..b628639 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@next/bundle-analyzer": "^16.1.6", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/typography": "^0.5.19", + "@tanstack/react-query": "^5.90.21", "@tiptap/extension-blockquote": "^3.20.1", "@tiptap/extension-bold": "^3.20.1", "@tiptap/extension-bullet-list": "^3.20.1", @@ -138,6 +139,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -662,6 +664,7 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" @@ -782,6 +785,7 @@ "resolved": "https://registry.npmjs.org/@icons-pack/react-simple-icons/-/react-simple-icons-13.12.0.tgz", "integrity": "sha512-RCtXJw+tTG8fmBieqBJSRA5BItVcXCPN6QOIqIJMmLD7ESLr8nB78JrahheXofy8ZWH+Fpwxou0oElseObu1dw==", "license": "MIT", + "peer": true, "engines": { "node": ">=24", "pnpm": ">=10" @@ -2355,11 +2359,38 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tiptap/core": { "version": "3.20.1", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.1.tgz", "integrity": "sha512-SwkPEWIfaDEZjC8SEIi4kZjqIYUbRgLUHUuQezo5GbphUNC8kM1pi3C3EtoOPtxXrEbY6e4pWEzW54Pcrd+rVA==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2591,6 +2622,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.20.1.tgz", "integrity": "sha512-/GPFSLNdYSZQ0E6VBXSAk0UFtDdn98HT1Aa2tO+STELqc5jAdFB42dfFnTC6KQzTvcUOUYkE2S1Q22YC5H2XNQ==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2634,6 +2666,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.1.tgz", "integrity": "sha512-euBRAn0mkV7R2VEE+AuOt3R0j9RHEMFXamPFmtvTo8IInxDClusrm6mJoDjS8gCGAXsQCRiAe1SCQBPgGbOOwg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2741,6 +2774,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.20.1.tgz", "integrity": "sha512-y2o1De3P/FH80nst1D9pYSwtRvxF87WkNAYGurqCkZ6SjwFq1Ifeh9eY+o79R6SCwcfpvQRytCYd+Yw9o5XAtA==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2885,6 +2919,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.1.tgz", "integrity": "sha512-JRc/v+OBH0qLTdvQ7HvHWTxGJH73QOf1MC0R8NhOX2QnAbg2mPFv1h+FjGa2gfLGuCXBdWQomjekWkUKbC4e5A==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2899,6 +2934,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.1.tgz", "integrity": "sha512-6kCiGLvpES4AxcEuOhb7HR7/xIeJWMjZlb6J7e8zpiIh5BoQc7NoRdctsnmFEjZvC19bIasccshHQ7H2zchWqw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -3155,6 +3191,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3164,6 +3201,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3238,6 +3276,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -3707,6 +3746,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4110,6 +4150,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4491,6 +4532,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -4976,6 +5018,7 @@ "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -5112,6 +5155,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5607,6 +5651,7 @@ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.0.tgz", "integrity": "sha512-w8hghCMQ4oq10j6aZh3U2yeEQv5K69O/seDI/41PK4HtgkLrcBovUNc0ayBC3UyyU7V1mrY2yLzvYdWJX9pGZQ==", "license": "MIT", + "peer": true, "dependencies": { "motion-dom": "^12.35.0", "motion-utils": "^12.29.2", @@ -6981,6 +7026,7 @@ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", "license": "ISC", + "peer": true, "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -7245,6 +7291,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", @@ -7310,17 +7357,6 @@ } } }, - "node_modules/next-intl/node_modules/@swc/helpers": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", - "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -9124,6 +9160,7 @@ "version": "4.0.3", "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9565,6 +9602,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9733,6 +9771,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -9762,6 +9801,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -9810,6 +9850,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz", "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -9861,6 +9902,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9870,6 +9912,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9905,6 +9948,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9994,7 +10038,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -10668,7 +10713,8 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -10730,6 +10776,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10939,6 +10986,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11161,7 +11209,8 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.1.tgz", "integrity": "sha512-NswPjVHxk0Q1F/VMRemCPUzSojjuHHisQrBqQiRXg7MVbe3f5vQ6r0rTTXA/a/neC/4hnOEC4YpXca4LpH0SUg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/webpack-bundle-analyzer": { "version": "4.10.1", @@ -11367,6 +11416,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 74168e4..293be9b 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,12 @@ "packages/*" ], "dependencies": { - "@internationalized/date": "^3.7.0", "@icons-pack/react-simple-icons": "^13.12.0", + "@internationalized/date": "^3.7.0", "@next/bundle-analyzer": "^16.1.6", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/typography": "^0.5.19", + "@tanstack/react-query": "^5.90.21", "@tiptap/extension-blockquote": "^3.20.1", "@tiptap/extension-bold": "^3.20.1", "@tiptap/extension-bullet-list": "^3.20.1", diff --git a/src/app/(main)/SplashDate.tsx b/src/app/(main)/SplashDate.tsx index c0c6883..7eeb79e 100644 --- a/src/app/(main)/SplashDate.tsx +++ b/src/app/(main)/SplashDate.tsx @@ -1,22 +1,10 @@ "use client"; -import { useEffect, useState } from "react"; -import { ActiveJamResponse, getCurrentJam } from "@/helpers/jam"; import { Text } from "bioloom-ui"; +import { useCurrentJam } from "@/hooks/queries"; export default function SplashDate() { - const [activeJamResponse, setActiveJamResponse] = - useState(null); - - useEffect(() => { - const fetchData = async () => { - const jamData = await getCurrentJam(); - - setActiveJamResponse(jamData); - }; - - fetchData(); - }, []); + const { data: activeJamResponse } = useCurrentJam(); const getOrdinalSuffix = (day: number): string => { if (day > 3 && day < 21) return "th"; diff --git a/src/app/(main)/about/page.tsx b/src/app/(main)/about/page.tsx index 057876f..a107333 100644 --- a/src/app/(main)/about/page.tsx +++ b/src/app/(main)/about/page.tsx @@ -1,10 +1,7 @@ "use client"; -import { getCurrentJam } from "@/helpers/jam"; -import { JamType } from "@/types/JamType"; import { format } from "date-fns"; import { Users } from "lucide-react"; -import { useEffect, useState } from "react"; import { toZonedTime } from "date-fns-tz"; import { useTheme } from "@/providers/SiteThemeProvider"; import { Accordion, AccordionItem } from "bioloom-ui"; @@ -13,24 +10,13 @@ import { Card } from "bioloom-ui"; import { Hstack, Stack, Vstack } from "bioloom-ui"; import { Icon } from "bioloom-ui"; import AboutLogo from "../AboutLogo"; +import { useCurrentJam } from "@/hooks/queries"; export default function AboutPage() { - const [jam, setJam] = useState(); + const { data: jamResponse } = useCurrentJam(); + const jam = jamResponse?.jam ?? null; const { colors } = useTheme(); - useEffect(() => { - loadUser(); - async function loadUser() { - try { - const jamResponse = await getCurrentJam(); - const currentJam = jamResponse?.jam; - setJam(currentJam); - } catch (error) { - console.error(error); - } - } - }, []); - return (
diff --git a/src/app/(main)/e/[slug]/page.tsx b/src/app/(main)/e/[slug]/page.tsx index 43fa315..052514c 100644 --- a/src/app/(main)/e/[slug]/page.tsx +++ b/src/app/(main)/e/[slug]/page.tsx @@ -2,26 +2,13 @@ import { Button } from "bioloom-ui"; import { Spinner } from "bioloom-ui"; -import { getEvent } from "@/requests/event"; -import { EventType } from "@/types/EventType"; +import { useEvent } from "@/hooks/queries"; import { BadgePlus, TimerIcon } from "lucide-react"; import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; export default function EventPage() { - const [event, setEvent] = useState(); const { slug } = useParams(); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const fetchData = async () => { - const response = await getEvent(`${slug}`); - setEvent((await response.json()).data); - setIsLoading(false); - }; - - fetchData(); - }, [slug]); + const { data: event, isLoading } = useEvent(`${slug}`); if (isLoading) return ; diff --git a/src/app/(main)/inbox/page.tsx b/src/app/(main)/inbox/page.tsx index 1a65749..7f5c269 100644 --- a/src/app/(main)/inbox/page.tsx +++ b/src/app/(main)/inbox/page.tsx @@ -5,43 +5,25 @@ import { Icon } from "bioloom-ui"; import { Hstack, Vstack } from "bioloom-ui"; import { Text } from "bioloom-ui"; import { handleApplication, handleInvite } from "@/helpers/team"; -import { getSelf } from "@/requests/user"; -import { UserType } from "@/types/UserType"; -import { useEffect, useState } from "react"; import TeamInviteNotification from "./TeamInviteNotification"; import { deleteNotification } from "@/helpers/notifications"; import TeamApplicationNotification from "./TeamApplicationNotification"; import GeneralNotification from "./GeneralNotification"; import CommentNotification from "./CommentNotification"; +import { useSelf } from "@/hooks/queries"; +import { useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "@/hooks/queries/queryKeys"; export default function InboxPage() { - const [user, setUser] = useState(); - - useEffect(() => { - (async () => { - try { - const self = await getSelf(); - const data = await self.json(); - setUser(data); - } catch (error) { - console.error(error); - } - })(); - }, []); + const { data: user } = useSelf(); + const queryClient = useQueryClient(); const notifications = user?.receivedNotifications ?? []; - const removeNotificationFromState = (id: number) => - setUser((prev) => - prev - ? { - ...prev, - receivedNotifications: prev.receivedNotifications.filter( - (n) => n.id !== id - ), - } - : prev - ); + const removeNotificationFromState = async (id: number) => { + // Invalidate user query to refetch notifications + await queryClient.invalidateQueries({ queryKey: queryKeys.user.self() }); + }; return ( @@ -62,7 +44,7 @@ export default function InboxPage() { {notifications.map((notification) => { const handleMarkRead = async (id: number) => { const res = await deleteNotification(id); - if (res.ok) removeNotificationFromState(id); + if (res.ok) await removeNotificationFromState(id); }; switch (notification.type) { diff --git a/src/app/providers.tsx b/src/app/providers.tsx index db94cfc..6e07b0b 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -11,6 +11,18 @@ import { merge } from "lodash"; import { AbstractIntlMessages, NextIntlClientProvider } from "next-intl"; import { useEffect, useState } from "react"; import { ShortcutProvider } from "react-keybind"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useTracks } from "@/hooks/queries"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + refetchOnWindowFocus: false, + retry: 1, + }, + }, +}); export default function Providers({ children, @@ -54,23 +66,25 @@ export default function Providers({ }, [previewLocale, locale, messages]); return ( - - - - - - - - - {children} - - - - - - + + + + + + + + + + {children} + + + + + + + ); } @@ -92,27 +106,13 @@ function BioloomThemeBridge({ children }: { children: React.ReactNode }) { function MusicTrackLoader() { const { setTracks } = useMusic(); + const { data: tracks } = useTracks(); useEffect(() => { - let cancelled = false; - - const loadTracks = async () => { - try { - const res = await fetch(`${BASE_URL}/tracks`); - const json = await res.json(); - if (!cancelled) { - setTracks(json.data ?? []); - } - } catch (error) { - console.error("Error loading tracks:", error); - } - }; - - loadTracks(); - return () => { - cancelled = true; - }; - }, [setTracks]); + if (tracks) { + setTracks(tracks); + } + }, [tracks, setTracks]); return null; } diff --git a/src/components/admin/AdminImages.tsx b/src/components/admin/AdminImages.tsx index c739a9f..a9d0c2a 100644 --- a/src/components/admin/AdminImages.tsx +++ b/src/components/admin/AdminImages.tsx @@ -1,8 +1,10 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; -import { getAdminImages } from "@/requests/admin"; +import { useMemo } from "react"; +import { useAdminImages } from "@/hooks/queries"; import { useTheme } from "@/providers/SiteThemeProvider"; +import { useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "@/hooks/queries/queryKeys"; import { Button, Card, @@ -43,41 +45,17 @@ const formatBytes = (value: number) => { }; export default function AdminImages() { - const [loading, setLoading] = useState(true); - const [data, setData] = useState(null); - const [error, setError] = useState(null); + const { data, isLoading: loading, isError } = useAdminImages(); + const typedData = data as AdminImagesResponse | undefined; const { colors } = useTheme(); - - const loadImages = async () => { - setLoading(true); - setError(null); - try { - const response = await getAdminImages(); - if (!response.ok) { - const json = await response.json().catch(() => null); - setError(json?.message ?? "Failed to load images"); - setData(null); - return; - } - const json = await response.json(); - setData(json.data as AdminImagesResponse); - } catch (err) { - console.error(err); - setError("Failed to load images"); - setData(null); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadImages(); - }, []); + const queryClient = useQueryClient(); const unusedCount = useMemo(() => { - if (!data) return 0; - return data.files.filter((file) => file.usageCount === 0).length; - }, [data]); + if (!typedData) return 0; + return typedData.files.filter((file) => file.usageCount === 0).length; + }, [typedData]); + + const error = isError ? "Failed to load images" : null; return (
@@ -91,7 +69,14 @@ export default function AdminImages() { Uploaded images with usage counts and cleanup status. - @@ -104,7 +89,7 @@ export default function AdminImages() { Total Files - {data?.totalFiles ?? 0} + {typedData?.totalFiles ?? 0} @@ -114,7 +99,7 @@ export default function AdminImages() { Total Size - {formatBytes(data?.totalSize ?? 0)} + {formatBytes(typedData?.totalSize ?? 0)} @@ -141,10 +126,10 @@ export default function AdminImages() { ) : ( - Deleted {data.deletedCount} stale files ( - {formatBytes(data.deletedSize)}). + Deleted {typedData.deletedCount} stale files ( + {formatBytes(typedData.deletedSize)}). ) : null } @@ -157,8 +142,8 @@ export default function AdminImages() { Last Modified - {data?.files?.length ? ( - data.files.map((file) => ( + {typedData?.files?.length ? ( + typedData.files.map((file) => (
([]); - const [activeJam, setActiveJam] = useState(null); - const [loading, setLoading] = useState(true); + const { data: activeJam, isLoading: activeJamLoading } = useCurrentJam(); + const { data: rawJams, isLoading: jamsLoading } = useJams(); - useEffect(() => { - let active = true; + const loading = activeJamLoading || jamsLoading; - const loadJams = async () => { - setLoading(true); - try { - const [jamList, currentJam] = await Promise.all([ - getJams(), - getCurrentJam(), - ]); - if (!active) return; - - const sorted = [...jamList].sort((a, b) => { - const aTime = a.startTime ? new Date(a.startTime).getTime() : 0; - const bTime = b.startTime ? new Date(b.startTime).getTime() : 0; - return bTime - aTime; - }); - - setJams(sorted); - setActiveJam(currentJam ?? null); - } catch (error) { - console.error("Failed to load jams", error); - if (!active) return; - setJams([]); - setActiveJam(null); - } finally { - if (active) setLoading(false); - } - }; - - loadJams(); - return () => { - active = false; - }; - }, []); + const jams = useMemo(() => { + const list = Array.isArray(rawJams) ? rawJams : []; + return [...list].sort((a, b) => { + const aTime = a.startTime ? new Date(a.startTime).getTime() : 0; + const bTime = b.startTime ? new Date(b.startTime).getTime() : 0; + return bTime - aTime; + }); + }, [rawJams]); const activeJamId = activeJam?.jam?.id; const totalJamCount = jams.length; diff --git a/src/components/admin/AdminThemeEliminationResults.tsx b/src/components/admin/AdminThemeEliminationResults.tsx index d8c6bf6..6620bc4 100644 --- a/src/components/admin/AdminThemeEliminationResults.tsx +++ b/src/components/admin/AdminThemeEliminationResults.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; -import { getThemes } from "@/requests/theme"; +import { useMemo } from "react"; +import { useThemes } from "@/hooks/queries"; import type { ThemeType } from "@/types/ThemeType"; import { Button, @@ -23,36 +23,8 @@ type ThemeWithScore = ThemeType & { }; export default function AdminThemeEliminationResults() { - const [themes, setThemes] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let active = true; - - const loadThemes = async () => { - setLoading(true); - try { - const response = await getThemes(true); - if (!active) return; - if (response.ok) { - const data = await response.json(); - setThemes(data.data ?? []); - } else { - setThemes([]); - } - } catch (error) { - console.error("Failed to load elimination results", error); - if (active) setThemes([]); - } finally { - if (active) setLoading(false); - } - }; - - loadThemes(); - return () => { - active = false; - }; - }, []); + const { data, isLoading: loading } = useThemes(true); + const themes: ThemeWithScore[] = data ?? []; const rankedThemes = useMemo(() => { return [...themes].sort( diff --git a/src/components/admin/AdminThemeSuggestions.tsx b/src/components/admin/AdminThemeSuggestions.tsx index 1a15061..57c2e26 100644 --- a/src/components/admin/AdminThemeSuggestions.tsx +++ b/src/components/admin/AdminThemeSuggestions.tsx @@ -1,7 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; -import { getThemes } from "@/requests/theme"; +import { useThemes } from "@/hooks/queries"; import type { ThemeType } from "@/types/ThemeType"; import { Button, @@ -19,36 +18,8 @@ import { } from "bioloom-ui"; export default function AdminThemeSuggestions() { - const [themes, setThemes] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let active = true; - - const loadThemes = async () => { - setLoading(true); - try { - const response = await getThemes(); - if (!active) return; - if (response.ok) { - const data = await response.json(); - setThemes(data.data ?? []); - } else { - setThemes([]); - } - } catch (error) { - console.error("Failed to load theme suggestions", error); - if (active) setThemes([]); - } finally { - if (active) setLoading(false); - } - }; - - loadThemes(); - return () => { - active = false; - }; - }, []); + const { data, isLoading: loading } = useThemes(false); + const themes: ThemeType[] = data ?? []; return (
diff --git a/src/components/admin/AdminThemeVotingResults.tsx b/src/components/admin/AdminThemeVotingResults.tsx index 511cef7..fad55e5 100644 --- a/src/components/admin/AdminThemeVotingResults.tsx +++ b/src/components/admin/AdminThemeVotingResults.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; -import { getThemes } from "@/requests/theme"; +import { useMemo } from "react"; +import { useThemes } from "@/hooks/queries"; import type { ThemeType } from "@/types/ThemeType"; import { Button, @@ -30,36 +30,8 @@ function formatVote(score?: number) { } export default function AdminThemeVotingResults() { - const [themes, setThemes] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let active = true; - - const loadThemes = async () => { - setLoading(true); - try { - const response = await getThemes(true); - if (!active) return; - if (response.ok) { - const data = await response.json(); - setThemes(data.data ?? []); - } else { - setThemes([]); - } - } catch (error) { - console.error("Failed to load voting themes", error); - if (active) setThemes([]); - } finally { - if (active) setLoading(false); - } - }; - - loadThemes(); - return () => { - active = false; - }; - }, []); + const { data, isLoading: loading } = useThemes(true); + const themes: ThemeWithScore[] = data ?? []; const rankedThemes = useMemo(() => { return [...themes].sort( diff --git a/src/components/events/index.tsx b/src/components/events/index.tsx index 2b101b9..ae0bf87 100644 --- a/src/components/events/index.tsx +++ b/src/components/events/index.tsx @@ -1,16 +1,12 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Avatar, Badge } from "bioloom-ui"; import { useRouter, useSearchParams } from "next/navigation"; import { EventFilter } from "@/types/EventFilter"; -import { getEvents } from "@/requests/event"; import { EventType } from "@/types/EventType"; import Link from "next/link"; import Timer from "../timers/Timer"; -import { UserType } from "@/types/UserType"; -import { hasCookie } from "@/helpers/cookie"; -import { getSelf } from "@/requests/user"; import { getIcon } from "@/helpers/icon"; import { Dropdown } from "bioloom-ui"; import { Button } from "bioloom-ui"; @@ -20,11 +16,12 @@ import { Text } from "bioloom-ui"; import { Spinner } from "bioloom-ui"; import { Hstack } from "bioloom-ui"; import { navigateToSearchIfChanged } from "@/helpers/navigation"; +import { useEvents, useSelf } from "@/hooks/queries"; +import { hasCookie } from "@/helpers/cookie"; export default function Events() { const searchParams = useSearchParams(); - const [events, setEvents] = useState(); const [filter, setFilter] = useState( (["upcoming", "current", "past"].includes( searchParams.get("filter") as EventFilter @@ -33,8 +30,12 @@ export default function Events() { "current" ); const router = useRouter(); - const [loading, setLoading] = useState(true); - const [user, setUser] = useState(); + + const hasToken = hasCookie("token"); + const { data: events, isLoading: eventsLoading } = useEvents(filter); + const { data: user, isLoading: userLoading } = useSelf(hasToken); + + const loading = eventsLoading || (hasToken && userLoading); const updateQueryParam = (key: string, value: string) => { const params = new URLSearchParams(window.location.search); @@ -46,37 +47,6 @@ export default function Events() { navigateToSearchIfChanged(router, params); }; - useEffect(() => { - const loadData = async () => { - setLoading(true); - - try { - // Fetch events - const eventsResponse = await getEvents(filter); - setEvents((await eventsResponse.json()).data); - - if (!hasCookie("token")) { - setUser(undefined); - return; - } - - const response = await getSelf(); - - if (response.status == 200) { - setUser(await response.json()); - } else { - setUser(undefined); - } - - setLoading(false); - } catch (error) { - console.error(error); - } - }; - - loadData(); - }, [filter]); - const filters: Record< EventFilter, { name: string; icon: IconName; description: string } @@ -133,7 +103,7 @@ export default function Events() {
{events && events.length > 0 ? (
- {events.map((event) => ( + {events.map((event: EventType) => ( (true); - const [games, setGames] = useState(); - const [user, setUser] = useState(); - const sortParam = (searchParams.get("sort") as GameSort) || "recommended"; const [sort, setSort] = useState( ([ @@ -230,13 +221,8 @@ export default function Games() { return p ?? "all"; }, []); const [jamId, setJamId] = useState(initialJamParam); - const [jamOptions, setJamOptions] = useState([]); const [jamDetecting, setJamDetecting] = useState(true); - const [hasData, setHasData] = useState(false); const [showBusy, setShowBusy] = useState(false); - const [currentJamId, setCurrentJamId] = useState( - undefined, - ); const initialTypeParam = useMemo(() => { if (typeof window === "undefined") return "all"; @@ -314,9 +300,100 @@ export default function Games() { { id: "Extra", name: "Extra", icon: "calendar" }, ]; + // Fetch user via TanStack Query + const { data: user } = useSelf(); + + // Fetch current jam and all jams via TanStack Query + const { data: currentJamData } = useCurrentJam(); + const { data: allJams } = useJams(); + + const currentJamId = currentJamData?.jam?.id?.toString(); + const isRestricted = (s: GameSort) => restrictedSorts.has(s); const canUseRestrictedSorts = !!currentJamId && jamId === currentJamId; + // Build jam options from query data + const jamOptions = useMemo(() => { + const options: JamOption[] = [ + { + id: "all", + name: "All Jams", + }, + ]; + + if (currentJamData?.jam) { + const cjId = currentJamData.jam.id?.toString(); + if (cjId) { + options.push({ + id: cjId, + name: currentJamData.jam.name || "Current Jam", + icon: currentJamData.jam.icon, + description: `${formatJamWindow( + currentJamData.jam.startTime, + currentJamData.jam.jammingHours + )}`, + }); + } + } + + if (Array.isArray(allJams)) { + allJams.forEach((j: { id?: number; name?: string; icon?: IconName; startTime?: string; jammingHours?: number }) => { + const id = String(j?.id ?? ""); + if (id && j?.name && !options.find((o) => o.id === id)) { + options.push({ + id, + name: j.name, + icon: j.icon, + description: formatJamWindow(j?.startTime, j?.jammingHours), + }); + } + }); + } + + return options; + }, [currentJamData, allJams]); + + // Handle jam detection and default selection + useEffect(() => { + if (!currentJamData && !allJams) return; // still loading + + const isRatingPhase = + currentJamData?.phase === "Rating" || + currentJamData?.phase === "Submission" || + currentJamData?.phase === "Jamming"; + + let ratingDefault: string | null = null; + if (isRatingPhase && (initialJamParam === "all" || !initialJamParam)) { + ratingDefault = currentJamId ?? null; + } + + if ( + !hasAppliedDefault.current && + !hasUserSelected.current && + ratingDefault + ) { + hasAppliedDefault.current = true; + setJamId(ratingDefault); + + const params = new URLSearchParams(window.location.search); + params.set("jam", ratingDefault); + const qs = params.toString(); + router.replace(qs ? `?${qs}` : "?"); + } + + setJamDetecting(false); + }, [currentJamData, allJams, router, initialJamParam, currentJamId]); + + // Fetch games via TanStack Query + const { data: games, isLoading: gamesLoading } = useGamesQuery( + sort, + jamId !== "all" ? jamId : undefined, + !jamDetecting + ); + + const hasData = Array.isArray(games); + const isLoading = gamesLoading; + const sorts: Record< GameSort, { name: string; icon: IconName; description: string } @@ -416,135 +493,11 @@ export default function Games() { } }, [sort, jamId, currentJamId, updateQueryParam, jamDetecting]); - useEffect(() => { - (async () => { - try { - const response = await getSelf(); - setUser(await response.json()); - } catch {} - })(); - }, []); - - useEffect(() => { - let cancelled = false; - - (async () => { - setJamDetecting(true); - const options: JamOption[] = [ - { - id: "all", - name: "All Jams", - }, - ]; - - let ratingDefault: string | null = null; - try { - const res = await getCurrentJam(); - const isRatingPhase = - res?.phase === "Rating" || - res?.phase === "Submission" || - res?.phase === "Jamming"; - const currentJamId = res?.jam?.id?.toString(); - const currentJamName = res?.jam?.name || "Current Jam"; - - setCurrentJamId(currentJamId || undefined); - - if (currentJamId) - options.push({ - id: currentJamId, - name: currentJamName, - icon: res?.jam?.icon, - description: `${formatJamWindow( - res?.jam?.startTime, - res?.jam?.jammingHours, - )}`, - }); - - if (isRatingPhase && (initialJamParam === "all" || !initialJamParam)) { - ratingDefault = currentJamId ?? null; - } - } catch {} - - try { - if (typeof getJams === "function") { - const jr = await getJams(); - const js = await jr.json(); - if (Array.isArray(js)) { - js.forEach((j) => { - const id = String(j?.id ?? ""); - if (id && j?.name && !options.find((o) => o.id === id)) { - options.push({ - id, - name: j.name, - icon: j.icon, - description: formatJamWindow(j?.startTime, j?.jammingHours), - }); - } - }); - } - } - } catch {} - - if (cancelled) return; - - setJamOptions(options); - - if ( - !hasAppliedDefault.current && - !hasUserSelected.current && - ratingDefault - ) { - hasAppliedDefault.current = true; - setJamId(ratingDefault); - - const params = new URLSearchParams(window.location.search); - params.set("jam", ratingDefault); - navigateToSearchIfChanged(router, params, "replace"); - } - - setJamDetecting(false); - })(); - - return () => { - cancelled = true; - }; - }, [router, initialJamParam]); - - useEffect(() => { - if (jamDetecting) return; - - let cancelled = false; - (async () => { - try { - setIsLoading(true); - const gameResponse = await getGames( - sort, - jamId !== "all" ? jamId : undefined, - ); - if (cancelled) return; - const data = await gameResponse.json(); - setGames(data); - if (Array.isArray(data)) setHasData(true); - } catch (error) { - if (!cancelled) { - console.error(error); - setGames(undefined); - } - } finally { - if (!cancelled) setIsLoading(false); - } - })(); - - return () => { - cancelled = true; - }; - }, [sort, jamId, jamDetecting]); - const tagOptions = useMemo(() => { if (!games) return []; const seen = new Map(); - games.forEach((game) => { + games.forEach((game: GameType) => { (game.tags ?? []).forEach((tag) => { const id = String(tag.id); if (!seen.has(id)) { @@ -567,7 +520,7 @@ export default function Games() { if (!games) return []; const used = new Set(); - games.forEach((game) => { + games.forEach((game: GameType) => { (game.inputMethods ?? []).forEach((method) => { if (method in INPUT_METHOD_OPTIONS) { used.add(method as InputMethodFilter); @@ -588,7 +541,7 @@ export default function Games() { if (!games) return []; const used = new Set(); - games.forEach((game) => { + games.forEach((game: GameType) => { getGameBuildTypes(game).forEach((buildType) => used.add(buildType)); }); @@ -605,7 +558,7 @@ export default function Games() { if (!games) return []; const seen = new Map(); - games.forEach((game) => { + games.forEach((game: GameType) => { (game.flags ?? []).forEach((flag) => { const id = String(flag.id); if (!seen.has(id)) { @@ -663,7 +616,7 @@ export default function Games() { const moveOwnGameToEnd = selectedMoreFilters.has("moveOwnGameToEnd"); const moveRatedGamesToEnd = selectedMoreFilters.has("moveRatedGamesToEnd"); - const filteredGames = games.filter((game) => { + return games.filter((game: GameType) => { if (hideOwnGame && user) { const isOwnGame = game.team?.ownerId === user.id || @@ -1013,7 +966,7 @@ export default function Games() {
{displayedGames && displayedGames.length > 0 ? ( - displayedGames.map((game) => ( + displayedGames.map((game: GameType) => (