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/packages/bioloom-miniplayer/src/MusicProvider.tsx b/packages/bioloom-miniplayer/src/MusicProvider.tsx index 1bd685f..7726a80 100644 --- a/packages/bioloom-miniplayer/src/MusicProvider.tsx +++ b/packages/bioloom-miniplayer/src/MusicProvider.tsx @@ -1,11 +1,12 @@ "use client"; -import { getCurrentJam } from "@/helpers/jam"; import RatingVisibilityGate from "@/components/ratings/RatingVisibilityGate"; import { useEffectiveHideRatings } from "@/hooks/useEffectiveHideRatings"; import { postTrackRating } from "@/requests/rating"; import { getTrackRatingCategories } from "@/requests/track"; -import { getSelf } from "@/requests/user"; +import { useCurrentJam, useSelf } from "@/hooks/queries"; +import { useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "@/hooks/queries/queryKeys"; import { emitTrackRatingSync, subscribeToTrackRatingSync, @@ -449,18 +450,39 @@ function MiniPlayer() { const [hoverRating, setHoverRating] = useState(0); const [selectedRating, setSelectedRating] = useState(0); const [ratingCategoryId, setRatingCategoryId] = useState(null); - const [viewerId, setViewerId] = useState(null); - const [viewerTeamGameIds, setViewerTeamGameIds] = useState([]); - const [viewerTrackRatings, setViewerTrackRatings] = useState< - Array<{ trackId: number; categoryId: number; value: number }> - >([]); - const [activeJamId, setActiveJamId] = useState(null); - const [activeJamPhase, setActiveJamPhase] = useState(null); const [savingRating, setSavingRating] = useState(false); - const [hideRatings, setHideRatings] = useState(false); - const [autoHideRatingsWhileStreaming, setAutoHideRatingsWhileStreaming] = - useState(false); - const [viewerTwitch, setViewerTwitch] = useState(null); + const queryClient = useQueryClient(); + + const { data: currentJamData } = useCurrentJam(); + const { data: userData } = useSelf(); + + const activeJamId = currentJamData?.jam?.id ?? null; + const activeJamPhase = currentJamData?.phase ?? null; + const viewerId = userData?.id ?? null; + const viewerTeamGameIds = useMemo( + () => + Array.isArray(userData?.teams) + ? userData.teams + .map((team: { game?: { id?: number } | null }) => team.game?.id) + .filter((id: number | undefined): id is number => + Number.isInteger(id), + ) + : [], + [userData], + ); + const viewerTrackRatings: Array<{ + trackId: number; + categoryId: number; + value: number; + }> = useMemo( + () => (Array.isArray(userData?.trackRatings) ? userData.trackRatings : []), + [userData], + ); + const hideRatings = Boolean(userData?.hideRatings); + const autoHideRatingsWhileStreaming = Boolean( + userData?.autoHideRatingsWhileStreaming, + ); + const viewerTwitch = userData?.twitch ?? null; const [minimized, setMinimized] = useState(() => { const stored = readStorage(storageKey.minimized); if (stored === "true") return true; @@ -586,50 +608,9 @@ function MiniPlayer() { (async () => { try { - const [userResponse, jamResponse, categoriesResponse] = - await Promise.all([ - getSelf().catch(() => null), - getCurrentJam().catch(() => null), - getTrackRatingCategories().catch(() => null), - ]); - + const categoriesResponse = await getTrackRatingCategories().catch(() => null); if (cancelled) return; - if (userResponse?.ok) { - const user = await userResponse.json(); - if (cancelled) return; - setViewerId(user?.id ?? null); - setViewerTeamGameIds( - Array.isArray(user?.teams) - ? user.teams - .map( - (team: { game?: { id?: number } | null }) => team.game?.id, - ) - .filter((id: number | undefined): id is number => - Number.isInteger(id), - ) - : [], - ); - setViewerTrackRatings( - Array.isArray(user?.trackRatings) ? user.trackRatings : [], - ); - setHideRatings(Boolean(user?.hideRatings)); - setAutoHideRatingsWhileStreaming( - Boolean(user?.autoHideRatingsWhileStreaming), - ); - setViewerTwitch(user?.twitch ?? null); - } else { - setViewerId(null); - setViewerTeamGameIds([]); - setViewerTrackRatings([]); - setHideRatings(false); - setAutoHideRatingsWhileStreaming(false); - setViewerTwitch(null); - } - - setActiveJamId(jamResponse?.jam?.id ?? null); - setActiveJamPhase(jamResponse?.phase ?? null); - if (categoriesResponse?.ok) { const payload = await categoriesResponse.json(); const overall = @@ -1036,13 +1017,7 @@ function MiniPlayer() { return; } - setViewerTrackRatings((prev) => - upsertTrackRatingRecord(prev, { - trackId, - categoryId: ratingCategoryId, - value, - }), - ); + queryClient.invalidateQueries({ queryKey: queryKeys.user.self() }); } finally { setSavingRating(false); } @@ -1131,13 +1106,7 @@ function MiniPlayer() { return; } - setViewerTrackRatings((prev) => - upsertTrackRatingRecord(prev, { - trackId, - categoryId: ratingCategoryId, - value: nextValue, - }), - ); + queryClient.invalidateQueries({ queryKey: queryKeys.user.self() }); } finally { setSavingRating(false); } @@ -1199,13 +1168,7 @@ function MiniPlayer() { return; } - setViewerTrackRatings((prev) => - upsertTrackRatingRecord(prev, { - trackId, - categoryId: ratingCategoryId, - value, - }), - ); + queryClient.invalidateQueries({ queryKey: queryKeys.user.self() }); } finally { setSavingRating(false); } 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)/g/[gameSlug]/ClientGamePage.tsx b/src/app/(main)/g/[gameSlug]/ClientGamePage.tsx index d38e43d..f90ba7c 100644 --- a/src/app/(main)/g/[gameSlug]/ClientGamePage.tsx +++ b/src/app/(main)/g/[gameSlug]/ClientGamePage.tsx @@ -37,7 +37,7 @@ import { postScore } from "@/requests/score"; import { postRating, postTrackRating } from "@/requests/rating"; import { RatingType } from "@/types/RatingType"; import { RatingCategoryType } from "@/types/RatingCategoryType"; -import { ActiveJamResponse, getCurrentJam } from "@/helpers/jam"; +import { useCurrentJam } from "@/hooks/queries"; import { Radar, RadarChart, @@ -248,8 +248,7 @@ export default function ClientGamePage({ const [trackOverallCategory, setTrackOverallCategory] = useState(null); const effectiveHideRatings = useEffectiveHideRatings(user); - const [activeJamResponse, setActiveJamResponse] = - useState(null); + const { data: activeJamResponse } = useCurrentJam(); const [isItchEmbedActive, setIsItchEmbedActive] = useState(false); const [currentMediaIndex, setCurrentMediaIndex] = useState(0); const [isScreenshotViewerOpen, setIsScreenshotViewerOpen] = useState(false); @@ -361,9 +360,6 @@ export default function ClientGamePage({ setTrackOverallCategory(overall); } - const jamData = await getCurrentJam(); - setActiveJamResponse(jamData); - // Fetch the logged-in user data if (getCookie("token")) { try { 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/(main)/m/[trackSlug]/ClientTrackPage.tsx b/src/app/(main)/m/[trackSlug]/ClientTrackPage.tsx index 704794f..a4a7f29 100644 --- a/src/app/(main)/m/[trackSlug]/ClientTrackPage.tsx +++ b/src/app/(main)/m/[trackSlug]/ClientTrackPage.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTheme } from "@/providers/SiteThemeProvider"; -import { ActiveJamResponse, getCurrentJam } from "@/helpers/jam"; +import { useCurrentJam } from "@/hooks/queries"; import { getTrack, getTrackRatingCategories, @@ -73,9 +73,7 @@ export default function ClientTrackPage({ {}, ); const [hoverCategory, setHoverCategory] = useState(null); - const [isDownloading, setIsDownloading] = useState(false); - const [activeJamResponse, setActiveJamResponse] = - useState(null); + const { data: activeJamResponse } = useCurrentJam(); const effectiveHideRatings = useEffectiveHideRatings(user); useEffect(() => { @@ -119,11 +117,6 @@ export default function ClientTrackPage({ ) ?? null; setOverallCategory(overall); } - - const jamData = await getCurrentJam(); - if (!cancelled) { - setActiveJamResponse(jamData); - } } catch (error) { console.error(error); } finally { diff --git a/src/app/(main)/music/page.tsx b/src/app/(main)/music/page.tsx index 734041a..03351c4 100644 --- a/src/app/(main)/music/page.tsx +++ b/src/app/(main)/music/page.tsx @@ -10,8 +10,8 @@ import { TrackType } from "@/types/TrackType"; import { GameSort } from "@/types/GameSort"; import { useEffect, useMemo, useRef, useState, useCallback } from "react"; import { useRouter } from "next/navigation"; -import { getCurrentJam } from "@/helpers/jam"; import { getJams } from "@/requests/jam"; +import { useCurrentJam } from "@/hooks/queries"; import { IconName } from "bioloom-ui"; import { getTrackRatingCategories, @@ -119,6 +119,7 @@ function formatJamWindow( export default function MusicPage() { const { colors } = useTheme(); const router = useRouter(); + const { data: currentJamData } = useCurrentJam(); const restrictedSorts = useMemo( () => new Set([ @@ -280,19 +281,19 @@ export default function MusicPage() { const options: JamOption[] = [{ id: "all", name: "All Jams" }]; let ratingDefault: string | null = null; - try { - const res = await getCurrentJam(); + { + const res = currentJamData; const isRatingPhase = res?.phase === "Rating" || res?.phase === "Submission" || res?.phase === "Jamming"; - const currentJamId = res?.jam?.id?.toString(); + const detectedJamId = res?.jam?.id?.toString(); const currentJamName = res?.jam?.name || "Current Jam"; - if (currentJamId) { - setCurrentJamId(currentJamId); + if (detectedJamId) { + setCurrentJamId(detectedJamId); options.push({ - id: currentJamId, + id: detectedJamId, name: currentJamName, icon: res?.jam?.icon, description: formatJamWindow( @@ -303,11 +304,11 @@ export default function MusicPage() { } if (isRatingPhase && (initialJamParam === "all" || !initialJamParam)) { - ratingDefault = currentJamId ?? null; + ratingDefault = detectedJamId ?? null; } setActiveJamPhase(res?.phase ?? null); - } catch {} + } try { if (typeof getJams === "function") { @@ -352,7 +353,7 @@ export default function MusicPage() { return () => { cancelled = true; }; - }, [router, initialJamParam]); + }, [router, initialJamParam, currentJamData]); const canUseRestrictedSorts = Boolean(currentJamId) && jamId === currentJamId; const isRestricted = useCallback( diff --git a/src/app/(main)/team/page.tsx b/src/app/(main)/team/page.tsx index a46c5b8..58b82c3 100644 --- a/src/app/(main)/team/page.tsx +++ b/src/app/(main)/team/page.tsx @@ -25,7 +25,7 @@ import { leaveTeam, } from "@/helpers/team"; import { TeamInviteType } from "@/types/TeamInviteType"; -import { ActiveJamResponse, getCurrentJam } from "@/helpers/jam"; +import { useCurrentJam } from "@/hooks/queries"; import { Card } from "bioloom-ui"; import { Text } from "bioloom-ui"; import { Button } from "bioloom-ui"; @@ -57,26 +57,11 @@ export default function EditTeamPage() { const [body, setBody] = useState(""); const [users, setUsers] = useState([]); const [invitations, setInvitations] = useState([]); - const [activeJamResponse, setActiveJamResponse] = - useState(null); + const { data: activeJamResponse } = useCurrentJam(); const [name, setName] = useState(""); const { colors } = useTheme(); const [hoveredUserId, setHoveredUserId] = useState(null); - // Fetch the current jam phase using helpers/jam - useEffect(() => { - const fetchCurrentJamPhase = async () => { - try { - const activeJam = await getCurrentJam(); - setActiveJamResponse(activeJam); // Set active jam details - } catch (error) { - console.error("Error fetching current jam:", error); - } finally { - } - }; - - fetchCurrentJamPhase(); - }, []); useEffect(() => { loadUser(); @@ -90,8 +75,7 @@ export default function EditTeamPage() { const response = await getSelf(); - const jamResponse = await getCurrentJam(); - const currentJam = jamResponse?.jam; + const currentJam = activeJamResponse?.jam; if (response.status == 200) { const data = await response.json(); @@ -139,7 +123,7 @@ export default function EditTeamPage() { console.error(error); } } - }, []); + }, [activeJamResponse]); function changeTeam(newid: number) { setSelectedTeam(newid); diff --git a/src/app/(main)/u/[slug]/ClientUserPage.tsx b/src/app/(main)/u/[slug]/ClientUserPage.tsx index b44e883..a8bfe3c 100644 --- a/src/app/(main)/u/[slug]/ClientUserPage.tsx +++ b/src/app/(main)/u/[slug]/ClientUserPage.tsx @@ -41,6 +41,7 @@ import { use, useEffect, useMemo, useState } from "react"; import { getCookie } from "@/helpers/cookie"; import { getTeamRoles } from "@/requests/team"; import { RoleType } from "@/types/RoleType"; +import { BASE_URL } from "@/requests/config"; type RarityTier = | "Abyssal" @@ -570,11 +571,7 @@ export default function ClientUserPage({ useEffect(() => { if (!isOwner) return; - fetch( - process.env.NEXT_PUBLIC_MODE === "PROD" - ? "https://d2jam.com/api/v1/pfps" - : "http://localhost:3005/api/v1/pfps", - ) + fetch(`${BASE_URL}/pfps`) .then((res) => res.json()) .then((data) => setDefaultPfps(data.data)) .catch((err) => console.error("Failed to load pfps", err)); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2af8e54..f3b986a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,9 @@ import "./globals.css"; import Providers from "./providers"; import { getLocale, getMessages } from "next-intl/server"; import { LanguagePreviewProvider } from "@/providers/LanguagePreviewProvider"; +import { QueryClient, dehydrate } from "@tanstack/react-query"; +import { getCurrentJam } from "@/helpers/jam"; +import { queryKeys } from "@/hooks/queries/queryKeys"; const inter = Inter({ subsets: ["latin"] }); @@ -54,11 +57,18 @@ export default async function RootLayout({ const locale = await getLocale(); const messages = await getMessages(); + const queryClient = new QueryClient(); + await queryClient.prefetchQuery({ + queryKey: queryKeys.jam.current(), + queryFn: getCurrentJam, + staleTime: 5 * 60 * 1000, + }); + return ( - + {children} diff --git a/src/app/providers.tsx b/src/app/providers.tsx index db94cfc..6682abc 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -11,15 +11,29 @@ import { merge } from "lodash"; import { AbstractIntlMessages, NextIntlClientProvider } from "next-intl"; import { useEffect, useState } from "react"; import { ShortcutProvider } from "react-keybind"; +import { QueryClient, QueryClientProvider, HydrationBoundary, type DehydratedState } 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, locale, messages, + dehydratedState, }: Readonly<{ children: React.ReactNode; locale: string; messages: AbstractIntlMessages; + dehydratedState?: DehydratedState; }>) { const { isMobile } = useBreakpoint(); const { previewLocale } = useLanguagePreview(); @@ -54,23 +68,27 @@ export default function Providers({ }, [previewLocale, locale, messages]); return ( - - - - - - - - - {children} - - - - - - + + + + + + + + + + + {children} + + + + + + + + ); } @@ -92,27 +110,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) => ( ([]); const [allTrackTags, setAllTrackTags] = useState([]); const [allTrackFlags, setAllTrackFlags] = useState([]); - const [activeJamResponse, setActiveJam] = useState( - null, - ); + const { data: activeJamResponse } = useCurrentJam(); const [loading, setLoading] = useState(true); const [title, setTitle] = useState(""); const [short, setShort] = useState(""); @@ -609,9 +607,6 @@ export default function GameEditingForm({ const trackFlagsData = await trackFlagsResponse.json(); setAllTrackFlags(trackFlagsData.data ?? []); - const activeJam = await getCurrentJam(); - setActiveJam(activeJam); - // always get *fresh* teams before deciding to create one await refreshTeams(); @@ -628,14 +623,14 @@ export default function GameEditingForm({ if (!localuser) return; const hasTeamForJam = - !!activeJam?.jam?.id && - localuser.teams.some((t) => t.jamId === activeJam.jam?.id); + !!activeJamResponse?.jam?.id && + localuser.teams.some((t) => t.jamId === activeJamResponse.jam?.id); if (!hasTeamForJam && !creatingTeamRef.current) { creatingTeamRef.current = true; const alreadyHas = teamsRef.current.some( - (t) => t.jamId === activeJam?.jam?.id, + (t) => t.jamId === activeJamResponse?.jam?.id, ); if (!alreadyHas) { const created = await createTeam(); // should return truthy or handle 409 @@ -656,7 +651,7 @@ export default function GameEditingForm({ } }; load(); - }, [refreshTeams]); + }, [refreshTeams, activeJamResponse]); const styles: StylesConfig< { diff --git a/src/components/gamecard/index.tsx b/src/components/gamecard/index.tsx index 6b36fa9..0181ebd 100644 --- a/src/components/gamecard/index.tsx +++ b/src/components/gamecard/index.tsx @@ -158,7 +158,7 @@ export function GameCard({ case "Web": return ; default: - return <>; + return null; } })} diff --git a/src/components/games/index.tsx b/src/components/games/index.tsx index d9a6106..bfa36b5 100644 --- a/src/components/games/index.tsx +++ b/src/components/games/index.tsx @@ -4,9 +4,6 @@ import { GameType } from "@/types/GameType"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { GameSort } from "@/types/GameSort"; import { useSearchParams, useRouter } from "next/navigation"; -import { getGames } from "@/requests/game"; -import { UserType } from "@/types/UserType"; -import { getSelf } from "@/requests/user"; import { IconName } from "bioloom-ui"; import { Dropdown } from "bioloom-ui"; import { GameCard } from "../gamecard"; @@ -16,9 +13,7 @@ import { Card } from "bioloom-ui"; import { Button } from "bioloom-ui"; import { Text } from "bioloom-ui"; import { PlatformType } from "@/types/DownloadLinkType"; - -import { getCurrentJam } from "@/helpers/jam"; -import { getJams } from "@/requests/jam"; +import { useSelf, useCurrentJam, useJams, useGames as useGamesQuery } from "@/hooks/queries"; import { navigateToSearchIfChanged } from "@/helpers/navigation"; type JamOption = { @@ -204,10 +199,6 @@ export default function Games() { const searchParams = useSearchParams(); const router = useRouter(); - const [isLoading, setIsLoading] = useState(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) => ( (null); + const { data: activeJamResponse } = useCurrentJam(); const [topTheme, setTopTheme] = useState(null); const [currentDate, setCurrentDate] = useState(new Date()); const { siteTheme, colors } = useTheme(); @@ -20,7 +20,7 @@ export default function JamHeader() { index: number, nextEventIndex: number, currentDate: Date, - eventDateObj: Date | null, + eventDateObj: Date | null | undefined, ) => { if ( eventDateObj && @@ -111,38 +111,27 @@ export default function JamHeader() { return { text: "" }; }; - // Fetch active jam details + // Fetch top theme when jam is in relevant phase useEffect(() => { - const fetchData = async () => { - const jamData = await getCurrentJam(); - - setActiveJamResponse(jamData); - - // If we're in Jamming phase, fetch top themes and pick the first one - if ( - (jamData?.phase === "Jamming" || - jamData?.phase === "Submission" || - jamData?.phase === "Rating") && - jamData.jam - ) { - try { - const response = await getTheme(); - - if (response.ok) { - const theme = (await response.json()).data; - setTopTheme(theme.suggestion); - } else { - console.error("Failed to fetch top themes.", response.status); - } - } catch (error) { - console.error("Error fetching top themes:", error); - } - } - }; - - fetchData(); + if ( + (activeJamResponse?.phase === "Jamming" || + activeJamResponse?.phase === "Submission" || + activeJamResponse?.phase === "Rating") && + activeJamResponse.jam + ) { + getTheme() + .then((response) => { + if (response.ok) return response.json(); + }) + .then((data) => { + if (data?.data) setTopTheme(data.data.suggestion); + }) + .catch((error) => console.error("Error fetching top themes:", error)); + } + }, [activeJamResponse]); - const timer = setInterval(() => setCurrentDate(new Date()), 1000 * 60); // Update every minute + useEffect(() => { + const timer = setInterval(() => setCurrentDate(new Date()), 1000 * 60); return () => clearInterval(timer); }, []); diff --git a/src/components/navbar/mobilebar/Mobilebar.tsx b/src/components/navbar/mobilebar/Mobilebar.tsx index eb3a054..7c3d770 100644 --- a/src/components/navbar/mobilebar/Mobilebar.tsx +++ b/src/components/navbar/mobilebar/Mobilebar.tsx @@ -7,9 +7,8 @@ import { Button, Navbar, NavbarItem } from "bioloom-ui"; import { useTheme } from "@/providers/SiteThemeProvider"; import { Dropdown } from "bioloom-ui"; import { Avatar } from "bioloom-ui"; -import { getSelf } from "@/requests/user"; -import { useEffect, useState } from "react"; -import { UserType } from "@/types/UserType"; +import { hasCookie } from "@/helpers/cookie"; +import { useSelf } from "@/hooks/queries"; type MobilebarProps = { isLoggedIn: boolean; @@ -20,30 +19,8 @@ export default function Mobilebar({ isLoggedIn }: MobilebarProps) { const hidden = direction === "down"; const { colors } = useTheme(); - const [user, setUser] = useState(); - - useEffect(() => { - if (!isLoggedIn) { - setUser(undefined); - return; - } - - async function loadData() { - try { - const response = await getSelf(); - const user = await response.json(); - if (response.status == 200) { - setUser(user); - } else { - setUser(undefined); - } - } catch (error) { - console.error(error); - } - } - - loadData(); - }, [isLoggedIn]); + const hasToken = hasCookie("token"); + const { data: user } = useSelf(hasToken); return ( (); - const [user, setUser] = useState(); - const [currentJamGame, setCurrentJamGame] = useState(null); + const hasToken = hasCookie("token"); + const { data: user } = useSelf(hasToken); + const { data: currentGameData } = useCurrentGame(hasToken); const { siteTheme } = useTheme(); + const [isInJam, setIsInJam] = useState(undefined); const currentJamTeams = jam ? (user?.teams ?? []).filter((team) => team.jamId == jam.id) : []; const currentJamTeam = currentJamTeams.find((team) => team.game?.published) ?? currentJamTeams[0]; + const currentJamGame: GameType | null = + currentGameData && currentGameData.length > 0 ? currentGameData[0] : null; - useEffect(() => { - if (!isLoggedIn) { - setUser(undefined); - setIsInJam(false); - setCurrentJamGame(null); - return; - } - - async function loadData() { - try { - const [userResponse, currentGameResponse] = await Promise.all([ - getSelf(), - getCurrentGame().catch(() => null), - ]); - const user = await userResponse.json(); - - if ( - jam && - user.jams.filter((userjam: JamType) => userjam.id == jam.id).length > - 0 - ) { - setIsInJam(true); - } else { - setIsInJam(false); - } - if (userResponse.status == 200) { - setUser(user); - } else { - setUser(undefined); - } - - if (currentGameResponse?.ok) { - const payload = await currentGameResponse.json().catch(() => null); - const games = Array.isArray(payload?.data) ? payload.data : []; - const preferredGame = - games.find((game: GameType) => game?.published) ?? games[0] ?? null; - setCurrentJamGame(preferredGame); - } else { - setCurrentJamGame(null); - } - } catch (error) { - console.error(error); - } - } - - loadData(); - }, [isLoggedIn, jam]); + // Derive isInJam from user + jam data + const computedIsInJam = + isInJam !== undefined + ? isInJam + : user && jam + ? (user.jams?.filter((userjam: JamType) => userjam.id == jam.id) + .length ?? 0) > 0 + : false; return ( )} - {user && isMdUp && jam && isInJam && ( + {user && isMdUp && jam && computedIsInJam && ( )} - {user && isMdUp && jam && !isInJam && ( + {user && isMdUp && jam && !computedIsInJam && ( { - const currentJamResponse = await getCurrentJam(); - const currentJam = currentJamResponse?.jam; - if (!currentJam) { + if (!jam) { addToast({ title: "Navbar.NoJamToast.Title", description: "Navbar.NoJamToast.Description", @@ -317,7 +285,7 @@ export default function PCbar({ isLoggedIn, languages }: PCbarProps) { }); return; } - if (await joinJam(currentJam.id)) { + if (await joinJam(jam.id)) { setIsInJam(true); } }} diff --git a/src/components/posts/index.tsx b/src/components/posts/index.tsx index b3141e6..4cd08f5 100644 --- a/src/components/posts/index.tsx +++ b/src/components/posts/index.tsx @@ -1,18 +1,14 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import PostCard from "./PostCard"; import { PostType } from "@/types/PostType"; import { addToast, Avatar } from "bioloom-ui"; import { PostSort } from "@/types/PostSort"; import { PostStyle } from "@/types/PostStyle"; -import { UserType } from "@/types/UserType"; import { PostTime } from "@/types/PostTimes"; import { TagType } from "@/types/TagType"; import StickyPostCard from "./StickyPostCard"; -import { getTags } from "@/requests/tag"; -import { getSelf } from "@/requests/user"; -import { getPosts } from "@/requests/post"; import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import LikeButton from "./LikeButton"; @@ -34,13 +30,12 @@ import { useTranslations } from "next-intl"; import MentionedContent from "../mentions/MentionedContent"; import PostReactions from "./PostReactions"; import { navigateToSearchIfChanged } from "@/helpers/navigation"; +import { useSelf, useTags, usePosts } from "@/hooks/queries"; export default function Posts() { const searchParams = useSearchParams(); const { siteTheme, colors } = useTheme(); - const [posts, setPosts] = useState(); - const [stickyPosts, setStickyPosts] = useState(); const [sort, setSort] = useState( (["newest", "oldest", "top"].includes( searchParams.get("sort") as PostSort @@ -73,12 +68,7 @@ export default function Posts() { (searchParams.get("style") as PostStyle)) || "Cozy" ); - const [user, setUser] = useState(); const [oldIsOpen, setOldIsOpen] = useState(null); - const [loading, setLoading] = useState(true); - const [tags, setTags] = useState<{ - [category: string]: { tags: TagType[]; priority: number }; - }>(); const [tagRules, setTagRules] = useState<{ [key: number]: number }>(); const [reduceMotion, setReduceMotion] = useState(false); const router = useRouter(); @@ -86,6 +76,48 @@ export default function Posts() { const [currentPost, setCurrentPost] = useState(0); const t = useTranslations(); + // TanStack Query hooks + const { data: user } = useSelf(); + const { data: rawTags } = useTags(); + const { data: posts, isLoading: postsLoading } = usePosts( + sort, + time, + false, + tagRules, + user?.slug + ); + const { data: stickyPosts, isLoading: stickyLoading } = usePosts( + sort, + time, + true, + tagRules, + user?.slug + ); + + const loading = postsLoading || stickyLoading; + + // Transform raw tags into categorized object + const tags = useMemo(() => { + if (!rawTags) return undefined; + const tagObject: { + [category: string]: { tags: TagType[]; priority: number }; + } = {}; + for (const tag of rawTags) { + if (tag.name == "D2Jam") continue; + if (tag.category) { + if (tag.category.name in tagObject) { + tagObject[tag.category.name].tags.push(tag); + } else { + tagObject[tag.category.name] = { + tags: [tag], + priority: tag.category.priority, + }; + } + } + } + return tagObject; + }, [rawTags]); + useEffect(() => { const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); setReduceMotion(mediaQuery.matches); @@ -137,74 +169,6 @@ export default function Posts() { navigateToSearchIfChanged(router, params); }; - useEffect(() => { - const loadUserAndPosts = async () => { - setLoading(true); - - try { - const tagResponse = await getTags(); - - if (tagResponse.ok) { - const tagObject: { - [category: string]: { tags: TagType[]; priority: number }; - } = {}; - - for (const tag of (await tagResponse.json()).data) { - if (tag.name == "D2Jam") { - continue; - } - - if (tag.category) { - if (tag.category.name in tagObject) { - tagObject[tag.category.name].tags.push(tag); - } else { - tagObject[tag.category.name] = { - tags: [tag], - priority: tag.category.priority, - }; - } - } - } - - setTags(tagObject); - } - - // Fetch the user - const userResponse = await getSelf(); - const userData = userResponse.ok - ? await userResponse.json() - : undefined; - setUser(userData); - - // Fetch posts (with userSlug if user is available) - const postsResponse = await getPosts( - sort, - time, - false, - tagRules, - userData?.slug - ); - setPosts(await postsResponse.json()); - - // Sticky posts - // Fetch posts (with userSlug if user is available) - const stickyPostsResponse = await getPosts( - sort, - time, - true, - tagRules, - userData?.slug - ); - setStickyPosts(await stickyPostsResponse.json()); - setLoading(false); - } catch (error) { - console.error(error); - } - }; - - loadUserAndPosts(); - }, [sort, time, tagRules]); - const sorts: Record< PostSort, { name: string; icon: IconName; description: string } @@ -302,7 +266,7 @@ export default function Posts() { stickyPosts && stickyPosts.length > 0 && ( - {stickyPosts.map((post) => ( + {stickyPosts.map((post: PostType) => ( ))} @@ -474,7 +438,7 @@ export default function Posts() { ) : ( {posts && posts.length > 0 ? ( - posts.map((post, index) => ( + posts.map((post: PostType, index: number) => (
- {posts[currentPost]?.comments.map((comment) => ( + {posts[currentPost]?.comments.map((comment: PostType["comments"][number]) => (
diff --git a/src/components/sidebar/SidebarEvents.tsx b/src/components/sidebar/SidebarEvents.tsx index 02e61e7..c43cd7a 100644 --- a/src/components/sidebar/SidebarEvents.tsx +++ b/src/components/sidebar/SidebarEvents.tsx @@ -1,8 +1,7 @@ "use client"; import { Avatar, Badge } from "bioloom-ui"; -import { useEffect, useState } from "react"; -import { getEvents } from "@/requests/event"; +import { useEvents } from "@/hooks/queries"; import { EventType } from "@/types/EventType"; import Timer from "../timers/Timer"; import Link from "next/link"; @@ -11,29 +10,20 @@ import { Text } from "bioloom-ui"; import { Card } from "bioloom-ui"; import { Hstack } from "bioloom-ui"; import { Button } from "bioloom-ui"; +import { useMemo } from "react"; export default function SidebarEvents() { - const [events, setEvents] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const { data: currentEvents, isLoading: isLoadingCurrent } = + useEvents("current"); + const { data: upcomingEvents, isLoading: isLoadingUpcoming } = + useEvents("upcoming"); - useEffect(() => { - async function fetchData() { - try { - const eventResponse = await getEvents("current"); - const eventResponse2 = await getEvents("upcoming"); - setEvents([ - ...(await eventResponse.json()).data, - ...(await eventResponse2.json()).data, - ]); - } catch (error) { - console.error(error); - } finally { - setIsLoading(false); - } - } + const isLoading = isLoadingCurrent || isLoadingUpcoming; - fetchData(); - }, []); + const events: EventType[] = useMemo( + () => [...(currentEvents ?? []), ...(upcomingEvents ?? [])], + [currentEvents, upcomingEvents] + ); if (isLoading) return <>; diff --git a/src/components/sidebar/SidebarGames.tsx b/src/components/sidebar/SidebarGames.tsx index 4201201..8274070 100644 --- a/src/components/sidebar/SidebarGames.tsx +++ b/src/components/sidebar/SidebarGames.tsx @@ -1,50 +1,34 @@ "use client"; -import { useEffect, useState } from "react"; import { GameType } from "@/types/GameType"; -import { getGames } from "@/requests/game"; import { useTheme } from "@/providers/SiteThemeProvider"; import Image from "next/image"; import { Text } from "bioloom-ui"; import { Button } from "bioloom-ui"; import Link from "next/link"; - -// 👇 Assumes you have these helpers available -import { getCurrentJam, ActiveJamResponse } from "@/helpers/jam"; +import { useCurrentJam, useGames } from "@/hooks/queries"; +import { useMemo } from "react"; export default function SidebarGames() { - const [games, setGames] = useState([]); - const [isLoading, setIsLoading] = useState(true); const { colors } = useTheme(); + const { data: activeJam } = useCurrentJam(); - useEffect(() => { - const fetchGameData = async () => { - try { - let jamId: string | undefined; - - try { - const active: ActiveJamResponse | null = await getCurrentJam(); - const phase = active?.phase ?? ""; - const isRatingWindow = - phase === "Jamming" || phase === "Submission" || phase === "Rating"; + const jamId = useMemo(() => { + const phase = activeJam?.phase ?? ""; + const isRatingWindow = + phase === "Jamming" || phase === "Submission" || phase === "Rating"; + if (activeJam?.jam?.id && isRatingWindow) { + return activeJam.jam.id.toString(); + } + return undefined; + }, [activeJam]); - if (active?.jam?.id && isRatingWindow) { - jamId = active.jam.id.toString(); - } - } catch {} + const { data: gamesData, isLoading } = useGames("karma", jamId); - const gameResponse = await getGames("recommended", jamId); - const data = await gameResponse.json(); - setGames(Array.isArray(data) ? data : data?.data ?? []); - } catch (error) { - console.error(error); - } finally { - setIsLoading(false); - } - }; - - fetchGameData(); - }, []); + const games: GameType[] = useMemo( + () => (Array.isArray(gamesData) ? gamesData : []), + [gamesData] + ); if (isLoading) return <>; if (games.length === 0) return <>; diff --git a/src/components/sidebar/SidebarMusic.tsx b/src/components/sidebar/SidebarMusic.tsx index 9ad3f91..7b1b5e1 100644 --- a/src/components/sidebar/SidebarMusic.tsx +++ b/src/components/sidebar/SidebarMusic.tsx @@ -4,56 +4,15 @@ import SidebarSong from "./SidebarSong"; import useHasMounted from "@/hooks/useHasMounted"; import { Text } from "bioloom-ui"; import { Button } from "bioloom-ui"; -import { useEffect, useMemo, useState } from "react"; -import { TrackType } from "@/types/TrackType"; -import { BASE_URL } from "@/requests/config"; - -import { getCurrentJam, ActiveJamResponse } from "@/helpers/jam"; +import { useMemo } from "react"; +import { useTracks } from "@/hooks/queries"; export default function SidebarMusic() { const hasMounted = useHasMounted(); - const [music, setMusic] = useState([]); - - useEffect(() => { - async function loadData() { - try { - let jamId: string | undefined; - try { - const active: ActiveJamResponse | null = await getCurrentJam(); - const phase = active?.phase ?? ""; - const inWindow = - phase === "Jamming" || phase === "Submission" || phase === "Rating"; - if (active?.jam?.id && inWindow) { - jamId = active.jam.id.toString(); - } - } catch {} - - const qs = new URLSearchParams(); - if (jamId) qs.set("jamId", jamId); - qs.set("sort", "recommended"); - - const res = await fetch( - `${BASE_URL}/tracks${qs.toString() ? `?${qs.toString()}` : ""}` - ); - const json = await res.json(); - const tracks: TrackType[] = Array.isArray(json) - ? json - : Array.isArray(json?.data) - ? json.data - : []; - - setMusic(tracks); - } catch (err) { - console.error(err); - setMusic([]); - } - } - - loadData(); - }, []); + const { data: music } = useTracks(); const featured = useMemo( - () => music.slice(0, 5), + () => [...(music ?? [])].sort(() => Math.random() - 0.5).slice(0, 5), [music] ); diff --git a/src/components/sidebar/SidebarStreams.tsx b/src/components/sidebar/SidebarStreams.tsx index 060d03b..33664e1 100644 --- a/src/components/sidebar/SidebarStreams.tsx +++ b/src/components/sidebar/SidebarStreams.tsx @@ -1,38 +1,21 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { FeaturedStreamerType } from "@/types/FeaturedStreamerType"; import NextImage from "next/image"; -import { getStreamers } from "@/requests/streamer"; import { Eye, Play } from "lucide-react"; import { useTheme } from "@/providers/SiteThemeProvider"; import { Tooltip } from "bioloom-ui"; import { Button } from "bioloom-ui"; import { Chip } from "bioloom-ui"; +import { useStreamers } from "@/hooks/queries"; export default function SidebarStreams() { - const [streamers, setStreamers] = useState([]); + const { data: streamers = [] as FeaturedStreamerType[], isLoading } = + useStreamers(); const [currentIndex, setCurrentIndex] = useState(0); // State to track the currently displayed streamer const { colors, siteTheme } = useTheme(); - useEffect(() => { - const fetchStreamers = async () => { - try { - const response = await getStreamers(); - if (!response.ok) { - throw new Error("Failed to fetch featured streamers"); - } - - const data: FeaturedStreamerType[] = (await response.json()).data; - setStreamers(data); - } catch (error) { - console.error("Error fetching featured streamers:", error); - } - }; - - fetchStreamers(); - }, []); - // Function to handle moving to the previous streamer const handlePrev = () => { @@ -62,7 +45,7 @@ export default function SidebarStreams() { ); }; - if (streamers.length === 0) { + if (isLoading || streamers.length === 0) { return
Loading featured streamers...
; } diff --git a/src/components/team-finder/index.tsx b/src/components/team-finder/index.tsx index ce78175..3573049 100644 --- a/src/components/team-finder/index.tsx +++ b/src/components/team-finder/index.tsx @@ -7,8 +7,7 @@ import { applyToTeam, createTeam } from "@/helpers/team"; import { redirect } from "next/navigation"; import { getSelf } from "@/requests/user"; import { UserType } from "@/types/UserType"; -import { getCurrentJam } from "@/helpers/jam"; -import { JamType } from "@/types/JamType"; +import { useCurrentJam } from "@/hooks/queries"; import { Card } from "bioloom-ui"; import { Button } from "bioloom-ui"; import { Text } from "bioloom-ui"; @@ -32,7 +31,8 @@ export default function TeamFinder() { const [user, setUser] = useState(); const [selectedTeam, setSelectedTeam] = useState(); const [sortSet, setSortSet] = useState(false); - const [jam, setJam] = useState(); + const { data: currentJamData } = useCurrentJam(); + const jam = currentJamData?.jam ?? null; const [isOpen, setIsOpen] = useState(false); useEffect(() => { @@ -40,9 +40,6 @@ export default function TeamFinder() { try { const self = await getSelf(); const data = await self.json(); - const jamResponse = await getCurrentJam(); - const currentJam = jamResponse?.jam; - setJam(currentJam); if (data.primaryRoles.length > 0) setFilter("Primary Role"); setUser(data); diff --git a/src/components/themes/theme-elimination.tsx b/src/components/themes/theme-elimination.tsx index 7888f03..7826992 100644 --- a/src/components/themes/theme-elimination.tsx +++ b/src/components/themes/theme-elimination.tsx @@ -1,11 +1,10 @@ "use client"; import { - ActiveJamResponse, - getCurrentJam, hasJoinedCurrentJam, joinJam, } from "@/helpers/jam"; +import { useCurrentJam } from "@/hooks/queries"; import { getThemes, postThemeSlaughterVote } from "@/requests/theme"; import { ThemeType } from "@/types/ThemeType"; import { useEffect, useRef, useState } from "react"; @@ -25,9 +24,7 @@ import { Dropdown } from "bioloom-ui"; export default function ThemeSlaughter() { const [themes, setThemes] = useState([]); - const [activeJamResponse, setActiveJam] = useState( - null - ); + const { data: activeJamResponse } = useCurrentJam(); const [phaseLoading, setPhaseLoading] = useState(true); const [currentTheme, setCurrentTheme] = useState(-1); const [hasJoined, setHasJoined] = useState(false); @@ -386,9 +383,6 @@ export default function ThemeSlaughter() { useEffect(() => { async function fetchData() { try { - const activeJam = await getCurrentJam(); - setActiveJam(activeJam); // Set active jam details - const joined = await hasJoinedCurrentJam(); setHasJoined(joined); } catch (error) { diff --git a/src/components/themes/theme-suggest.tsx b/src/components/themes/theme-suggest.tsx index be1ecbf..f3ebd22 100644 --- a/src/components/themes/theme-suggest.tsx +++ b/src/components/themes/theme-suggest.tsx @@ -3,10 +3,9 @@ import { useState, useEffect, useRef } from "react"; import { getCookie } from "@/helpers/cookie"; import { - getCurrentJam, hasJoinedCurrentJam, - ActiveJamResponse, } from "@/helpers/jam"; +import { useCurrentJam } from "@/hooks/queries"; import { ThemeType } from "@/types/ThemeType"; import { joinJam } from "@/helpers/jam"; import { @@ -119,29 +118,18 @@ export default function ThemeSuggestions() { const [userSuggestions, setUserSuggestions] = useState([]); const [themeLimit, setThemeLimit] = useState(0); const [hasJoined, setHasJoined] = useState(false); - const [activeJamResponse, setActiveJamResponse] = - useState(null); + const { data: activeJamResponse, isLoading: jamLoading } = useCurrentJam(); const [phaseLoading, setPhaseLoading] = useState(true); // Loading state for fetching phase const inputRef = useRef(null); - // Fetch the current jam phase using helpers/jam + // Derive theme limit and loading from hook data useEffect(() => { - const fetchCurrentJamPhase = async () => { - try { - const activeJam = await getCurrentJam(); - setActiveJamResponse(activeJam); // Set active jam details - if (activeJam?.jam) { - setThemeLimit(activeJam.jam.themePerUser || Infinity); // Set theme limit - } - } catch (error) { - console.error("Error fetching current jam:", error); - } finally { - setPhaseLoading(false); // Stop loading when phase is fetched - } - }; - - fetchCurrentJamPhase(); - }, []); + if (jamLoading) return; + if (activeJamResponse?.jam) { + setThemeLimit(activeJamResponse.jam.themePerUser || Infinity); + } + setPhaseLoading(false); + }, [activeJamResponse, jamLoading]); // Fetch all suggestions for the logged-in user const fetchSuggestions = async () => { diff --git a/src/components/themes/theme-vote.tsx b/src/components/themes/theme-vote.tsx index da5e04f..49179e7 100644 --- a/src/components/themes/theme-vote.tsx +++ b/src/components/themes/theme-vote.tsx @@ -2,11 +2,10 @@ import { getCookie } from "@/helpers/cookie"; import { - ActiveJamResponse, - getCurrentJam, hasJoinedCurrentJam, joinJam, } from "@/helpers/jam"; +import { useCurrentJam } from "@/hooks/queries"; import { ThemeType } from "@/types/ThemeType"; import { useEffect, useState } from "react"; import { getThemes, postThemeVotingVote } from "@/requests/theme"; @@ -19,18 +18,13 @@ import { Icon } from "bioloom-ui"; export default function VotingPage() { const [themes, setThemes] = useState([]); - const [activeJamResponse, setActiveJam] = useState( - null - ); + const { data: activeJamResponse } = useCurrentJam(); const [phaseLoading, setPhaseLoading] = useState(true); const [hasJoined, setHasJoined] = useState(false); useEffect(() => { async function fetchData() { try { - const activeJam = await getCurrentJam(); - setActiveJam(activeJam); // Set active jam details - const joined = await hasJoinedCurrentJam(); setHasJoined(joined); } catch (error) { diff --git a/src/components/timers/index.tsx b/src/components/timers/index.tsx index f89119a..205c20a 100644 --- a/src/components/timers/index.tsx +++ b/src/components/timers/index.tsx @@ -1,29 +1,11 @@ "use client"; -import { useState, useEffect } from "react"; import Timer from "./Timer"; -import { getCurrentJam, ActiveJamResponse } from "@/helpers/jam"; import { useTheme } from "@/providers/SiteThemeProvider"; import useHasMounted from "@/hooks/useHasMounted"; +import { useCurrentJam } from "@/hooks/queries"; export default function Timers() { - const [activeJamResponse, setActiveJamResponse] = - useState(null); - - // Fetch the current jam phase using helpers/jam - useEffect(() => { - const fetchCurrentJamPhase = async () => { - try { - const activeJam = await getCurrentJam(); - setActiveJamResponse(activeJam); // Set active jam details - } catch (error) { - console.error("Error fetching current jam:", error); - } finally { - } - }; - - fetchCurrentJamPhase(); - }, []); - + const { data: activeJamResponse } = useCurrentJam(); const { siteTheme } = useTheme(); const hasMounted = useHasMounted(); diff --git a/src/hooks/queries/helpers.ts b/src/hooks/queries/helpers.ts new file mode 100644 index 0000000..92d6ef5 --- /dev/null +++ b/src/hooks/queries/helpers.ts @@ -0,0 +1,19 @@ +/** + * Unwrap API responses that may be either raw data or wrapped in { data: ... }. + * Some endpoints return raw arrays/objects, others wrap in { message, data }. + */ +export function unwrapArray(json: unknown): T[] { + if (Array.isArray(json)) return json; + if (json && typeof json === "object" && "data" in json) { + const d = (json as Record).data; + return Array.isArray(d) ? d : []; + } + return []; +} + +export function unwrapItem(json: unknown): T | null { + if (json && typeof json === "object" && "data" in json) { + return ((json as Record).data as T) ?? null; + } + return (json as T) ?? null; +} diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts new file mode 100644 index 0000000..0f890d8 --- /dev/null +++ b/src/hooks/queries/index.ts @@ -0,0 +1,11 @@ +export * from "./queryKeys"; +export * from "./helpers"; +export * from "./useJamQueries"; +export * from "./useUserQueries"; +export * from "./useGameQueries"; +export * from "./usePostQueries"; +export * from "./useEventQueries"; +export * from "./useThemeQueries"; +export * from "./useTeamQueries"; +export * from "./useEmojiQueries"; +export * from "./useMiscQueries"; diff --git a/src/hooks/queries/queryKeys.ts b/src/hooks/queries/queryKeys.ts new file mode 100644 index 0000000..e6f1fe6 --- /dev/null +++ b/src/hooks/queries/queryKeys.ts @@ -0,0 +1,115 @@ +export const queryKeys = { + jam: { + all: ["jam"] as const, + current: () => [...queryKeys.jam.all, "current"] as const, + list: () => [...queryKeys.jam.all, "list"] as const, + participation: () => [...queryKeys.jam.all, "participation"] as const, + }, + user: { + all: ["user"] as const, + self: () => [...queryKeys.user.all, "self"] as const, + detail: (slug: string) => [...queryKeys.user.all, "detail", slug] as const, + search: (query: string) => + [...queryKeys.user.all, "search", query] as const, + }, + game: { + all: ["game"] as const, + detail: (slug: string) => [...queryKeys.game.all, "detail", slug] as const, + list: (sort: string, jamId?: string) => + [...queryKeys.game.all, "list", sort, jamId] as const, + current: () => [...queryKeys.game.all, "current"] as const, + ratingCategories: (always?: boolean) => + [...queryKeys.game.all, "ratingCategories", always] as const, + flags: () => [...queryKeys.game.all, "flags"] as const, + tags: () => [...queryKeys.game.all, "tags"] as const, + results: ( + category: string, + contentType: string, + sort: string, + jam: string + ) => + [ + ...queryKeys.game.all, + "results", + category, + contentType, + sort, + jam, + ] as const, + }, + post: { + all: ["post"] as const, + list: ( + sort: string, + time: string, + sticky: boolean, + tagRules?: Record, + userSlug?: string + ) => + [ + ...queryKeys.post.all, + "list", + sort, + time, + sticky, + tagRules, + userSlug, + ] as const, + detail: (slug: string, userSlug?: string) => + [...queryKeys.post.all, "detail", slug, userSlug] as const, + }, + event: { + all: ["event"] as const, + list: (filter: string) => + [...queryKeys.event.all, "list", filter] as const, + detail: (slug: string) => + [...queryKeys.event.all, "detail", slug] as const, + }, + theme: { + all: ["theme"] as const, + current: () => [...queryKeys.theme.all, "current"] as const, + suggestions: () => [...queryKeys.theme.all, "suggestions"] as const, + list: (isVoting?: boolean) => + [...queryKeys.theme.all, "list", isVoting] as const, + votes: () => [...queryKeys.theme.all, "votes"] as const, + }, + emoji: { + all: ["emoji"] as const, + list: () => [...queryKeys.emoji.all, "list"] as const, + }, + team: { + all: ["team"] as const, + list: () => [...queryKeys.team.all, "list"] as const, + user: () => [...queryKeys.team.all, "user"] as const, + roles: () => [...queryKeys.team.all, "roles"] as const, + }, + tag: { + all: ["tag"] as const, + list: () => [...queryKeys.tag.all, "list"] as const, + }, + streamer: { + all: ["streamer"] as const, + list: () => [...queryKeys.streamer.all, "list"] as const, + }, + siteTheme: { + all: ["siteTheme"] as const, + list: () => [...queryKeys.siteTheme.all, "list"] as const, + }, + track: { + all: ["track"] as const, + list: () => [...queryKeys.track.all, "list"] as const, + }, + admin: { + all: ["admin"] as const, + images: () => [...queryKeys.admin.all, "images"] as const, + }, + notification: { + all: ["notification"] as const, + list: () => [...queryKeys.notification.all, "list"] as const, + }, + search: { + all: ["search"] as const, + results: (query: string) => + [...queryKeys.search.all, "results", query] as const, + }, +} as const; diff --git a/src/hooks/queries/useEmojiQueries.ts b/src/hooks/queries/useEmojiQueries.ts new file mode 100644 index 0000000..090af22 --- /dev/null +++ b/src/hooks/queries/useEmojiQueries.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getEmojis } from "@/requests/emoji"; +import { queryKeys } from "./queryKeys"; +import { useMemo } from "react"; +import type { ReactionType } from "@/types/ReactionType"; + +export type EmojiType = ReactionType; + +export function useEmojisQuery() { + const query = useQuery({ + queryKey: queryKeys.emoji.list(), + queryFn: async () => { + const res = await getEmojis(); + const data = await res.json(); + return (Array.isArray(data?.data) ? data.data : []) as EmojiType[]; + }, + staleTime: 5 * 60 * 1000, + }); + + const emojiMap = useMemo(() => { + const map: Record = {}; + (query.data ?? []).forEach((emoji) => { + map[emoji.slug] = emoji; + }); + return map; + }, [query.data]); + + return { + ...query, + emojis: query.data ?? [], + emojiMap, + }; +} diff --git a/src/hooks/queries/useEventQueries.ts b/src/hooks/queries/useEventQueries.ts new file mode 100644 index 0000000..0faaa29 --- /dev/null +++ b/src/hooks/queries/useEventQueries.ts @@ -0,0 +1,31 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getEvents, getEvent } from "@/requests/event"; +import { queryKeys } from "./queryKeys"; +import { unwrapArray, unwrapItem } from "./helpers"; +import type { EventType } from "@/types/EventType"; + +export function useEvents(filter: string, enabled = true) { + return useQuery({ + queryKey: queryKeys.event.list(filter), + queryFn: async () => { + const res = await getEvents(filter); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useEvent(slug: string, enabled = true) { + return useQuery({ + queryKey: queryKeys.event.detail(slug), + queryFn: async () => { + const res = await getEvent(slug); + const json = await res.json(); + return unwrapItem(json)!; + }, + enabled: enabled && !!slug, + }); +} diff --git a/src/hooks/queries/useGameQueries.ts b/src/hooks/queries/useGameQueries.ts new file mode 100644 index 0000000..bfb5d7d --- /dev/null +++ b/src/hooks/queries/useGameQueries.ts @@ -0,0 +1,109 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { + getCurrentGame, + getGame, + getGames, + getRatingCategories, + getFlags, + getGameTags, + getResults, +} from "@/requests/game"; +import { queryKeys } from "./queryKeys"; +import { unwrapArray, unwrapItem } from "./helpers"; +import type { GameType } from "@/types/GameType"; +import type { RatingCategoryType } from "@/types/RatingCategoryType"; +import type { FlagType } from "@/types/FlagType"; +import type { GameTagType } from "@/types/GameTagType"; +import type { GameResultType } from "@/types/GameResultType"; + +export function useCurrentGame(enabled = true) { + return useQuery({ + queryKey: queryKeys.game.current(), + queryFn: async () => { + const res = await getCurrentGame(); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useGame(slug: string, enabled = true) { + return useQuery({ + queryKey: queryKeys.game.detail(slug), + queryFn: async () => { + const res = await getGame(slug); + const json = await res.json(); + return unwrapItem(json)!; + }, + enabled: enabled && !!slug, + }); +} + +export function useGames(sort: string, jamId?: string, enabled = true) { + return useQuery({ + queryKey: queryKeys.game.list(sort, jamId), + queryFn: async () => { + const res = await getGames(sort, jamId); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useRatingCategories(always = false) { + return useQuery({ + queryKey: queryKeys.game.ratingCategories(always), + queryFn: async () => { + const res = await getRatingCategories(always); + const json = await res.json(); + return unwrapArray(json); + }, + staleTime: 10 * 60 * 1000, + }); +} + +export function useFlags() { + return useQuery({ + queryKey: queryKeys.game.flags(), + queryFn: async () => { + const res = await getFlags(); + const json = await res.json(); + return unwrapArray(json); + }, + staleTime: 10 * 60 * 1000, + }); +} + +export function useGameTags() { + return useQuery({ + queryKey: queryKeys.game.tags(), + queryFn: async () => { + const res = await getGameTags(); + const json = await res.json(); + return unwrapArray(json); + }, + staleTime: 10 * 60 * 1000, + }); +} + +export function useResults( + category: string, + contentType: string, + sort: string, + jam: string, + enabled = true +) { + return useQuery({ + queryKey: queryKeys.game.results(category, contentType, sort, jam), + queryFn: async () => { + const res = await getResults(category, contentType, sort, jam); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} diff --git a/src/hooks/queries/useJamQueries.ts b/src/hooks/queries/useJamQueries.ts new file mode 100644 index 0000000..a73bebe --- /dev/null +++ b/src/hooks/queries/useJamQueries.ts @@ -0,0 +1,39 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getCurrentJam, type ActiveJamResponse } from "@/helpers/jam"; +import * as jamRequests from "@/requests/jam"; +import { queryKeys } from "./queryKeys"; +import type { JamType } from "@/types/JamType"; +import { unwrapArray, unwrapItem } from "./helpers"; + +export function useCurrentJam() { + return useQuery({ + queryKey: queryKeys.jam.current(), + queryFn: getCurrentJam, + staleTime: 5 * 60 * 1000, + }); +} + +export function useJams() { + return useQuery({ + queryKey: queryKeys.jam.list(), + queryFn: async () => { + const res = await jamRequests.getJams(); + const json = await res.json(); + return unwrapArray(json); + }, + }); +} + +export function useHasJoinedCurrentJam(enabled = true) { + return useQuery({ + queryKey: queryKeys.jam.participation(), + queryFn: async () => { + const res = await jamRequests.hasJoinedCurrentJam(); + const data = await res.json(); + return (unwrapItem(data) ?? false); + }, + enabled, + }); +} diff --git a/src/hooks/queries/useMiscQueries.ts b/src/hooks/queries/useMiscQueries.ts new file mode 100644 index 0000000..1b33812 --- /dev/null +++ b/src/hooks/queries/useMiscQueries.ts @@ -0,0 +1,87 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getTags } from "@/requests/tag"; +import { getStreamers } from "@/requests/streamer"; +import { getAdminImages } from "@/requests/admin"; +import { BASE_URL } from "@/requests/config"; +import { queryKeys } from "./queryKeys"; +import type { TagType } from "@/types/TagType"; +import type { FeaturedStreamerType } from "@/types/FeaturedStreamerType"; +import type { TrackType } from "@/types/TrackType"; +import type { SiteThemeType } from "@/types/SiteThemeType"; +import { unwrapArray } from "./helpers"; + +export function useTags() { + return useQuery({ + queryKey: queryKeys.tag.list(), + queryFn: async () => { + const res = await getTags(); + const json = await res.json(); + return unwrapArray(json); + }, + staleTime: 10 * 60 * 1000, + }); +} + +export function useStreamers(enabled = true) { + return useQuery({ + queryKey: queryKeys.streamer.list(), + queryFn: async () => { + const res = await getStreamers(); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useAdminImages(enabled = true) { + return useQuery({ + queryKey: queryKeys.admin.images(), + queryFn: async () => { + const res = await getAdminImages(); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useTracks() { + return useQuery({ + queryKey: queryKeys.track.list(), + queryFn: async () => { + const res = await fetch(`${BASE_URL}/tracks`); + const data = await res.json(); + return data.data ?? []; + }, + staleTime: 5 * 60 * 1000, + }); +} + +export function useSiteThemes() { + return useQuery({ + queryKey: queryKeys.siteTheme.list(), + queryFn: async () => { + const res = await fetch(`${BASE_URL}/site-themes`); + const json = await res.json(); + return unwrapArray(json); + }, + staleTime: 10 * 60 * 1000, + }); +} + +export function useSearch(query: string, enabled = true) { + return useQuery({ + queryKey: queryKeys.search.results(query), + queryFn: async () => { + const res = await fetch( + `${BASE_URL}/search?q=${encodeURIComponent(query)}` + ); + const json = await res.json(); + return json?.data ?? json ?? null; + }, + enabled: enabled && query.length > 0, + }); +} diff --git a/src/hooks/queries/usePostQueries.ts b/src/hooks/queries/usePostQueries.ts new file mode 100644 index 0000000..2a85cd1 --- /dev/null +++ b/src/hooks/queries/usePostQueries.ts @@ -0,0 +1,40 @@ +"use client"; + +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { getPosts, getPost } from "@/requests/post"; +import { PostTime } from "@/types/PostTimes"; +import { queryKeys } from "./queryKeys"; +import { unwrapArray, unwrapItem } from "./helpers"; +import type { PostType } from "@/types/PostType"; + +export function usePosts( + sort: string, + time: PostTime, + sticky: boolean, + tagRules?: Record, + userSlug?: string, + enabled = true +) { + return useQuery({ + queryKey: queryKeys.post.list(sort, time, sticky, tagRules, userSlug), + queryFn: async () => { + const res = await getPosts(sort, time, sticky, tagRules, userSlug); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + placeholderData: keepPreviousData, + }); +} + +export function usePost(slug: string, userSlug?: string, enabled = true) { + return useQuery({ + queryKey: queryKeys.post.detail(slug, userSlug), + queryFn: async () => { + const res = await getPost(slug, userSlug); + const json = await res.json(); + return unwrapItem(json)!; + }, + enabled: enabled && !!slug, + }); +} diff --git a/src/hooks/queries/useTeamQueries.ts b/src/hooks/queries/useTeamQueries.ts new file mode 100644 index 0000000..ed31351 --- /dev/null +++ b/src/hooks/queries/useTeamQueries.ts @@ -0,0 +1,44 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getTeams, getTeamsUser, getTeamRoles } from "@/requests/team"; +import { queryKeys } from "./queryKeys"; +import type { TeamType } from "@/types/TeamType"; +import type { RoleType } from "@/types/RoleType"; +import { unwrapArray } from "./helpers"; + +export function useTeams(enabled = true) { + return useQuery({ + queryKey: queryKeys.team.list(), + queryFn: async () => { + const res = await getTeams(); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useTeamsUser(enabled = true) { + return useQuery({ + queryKey: queryKeys.team.user(), + queryFn: async () => { + const res = await getTeamsUser(); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useTeamRoles() { + return useQuery({ + queryKey: queryKeys.team.roles(), + queryFn: async () => { + const res = await getTeamRoles(); + const json = await res.json(); + return unwrapArray(json); + }, + staleTime: 10 * 60 * 1000, + }); +} diff --git a/src/hooks/queries/useThemeQueries.ts b/src/hooks/queries/useThemeQueries.ts new file mode 100644 index 0000000..98fc52c --- /dev/null +++ b/src/hooks/queries/useThemeQueries.ts @@ -0,0 +1,59 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { + getThemeSuggestions, + getTheme, + getThemes, + getThemeVotes, +} from "@/requests/theme"; +import { queryKeys } from "./queryKeys"; +import type { ThemeType } from "@/types/ThemeType"; +import { unwrapArray, unwrapItem } from "./helpers"; + +export function useTheme() { + return useQuery({ + queryKey: queryKeys.theme.current(), + queryFn: async () => { + const res = await getTheme(); + const json = await res.json(); + return unwrapItem(json); + }, + }); +} + +export function useThemeSuggestions(enabled = true) { + return useQuery({ + queryKey: queryKeys.theme.suggestions(), + queryFn: async () => { + const res = await getThemeSuggestions(); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useThemes(isVoting = false, enabled = true) { + return useQuery({ + queryKey: queryKeys.theme.list(isVoting), + queryFn: async () => { + const res = await getThemes(isVoting); + const json = await res.json(); + return unwrapArray(json); + }, + enabled, + }); +} + +export function useThemeVotes(enabled = true) { + return useQuery({ + queryKey: queryKeys.theme.votes(), + queryFn: async () => { + const res = await getThemeVotes(); + const json = await res.json(); + return unwrapItem(json); + }, + enabled, + }); +} diff --git a/src/hooks/queries/useUserQueries.ts b/src/hooks/queries/useUserQueries.ts new file mode 100644 index 0000000..df8e397 --- /dev/null +++ b/src/hooks/queries/useUserQueries.ts @@ -0,0 +1,48 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getSelf, getUser, searchUsers } from "@/requests/user"; +import { queryKeys } from "./queryKeys"; +import type { UserType } from "@/types/UserType"; +import { unwrapArray, unwrapItem } from "./helpers"; + +export function useSelf(enabled = true) { + return useQuery({ + queryKey: queryKeys.user.self(), + queryFn: async () => { + const res = await getSelf(); + if (!res.ok) throw new Error("Not authenticated"); + const json = await res.json(); + const user = unwrapItem(json); + if (!user) throw new Error("No user data"); + return user; + }, + retry: false, + enabled, + staleTime: 2 * 60 * 1000, + }); +} + +export function useUser(slug: string, enabled = true) { + return useQuery({ + queryKey: queryKeys.user.detail(slug), + queryFn: async () => { + const res = await getUser(slug); + const json = await res.json(); + return unwrapItem(json)!; + }, + enabled: enabled && !!slug, + }); +} + +export function useSearchUsers(query: string, enabled = true) { + return useQuery({ + queryKey: queryKeys.user.search(query), + queryFn: async () => { + const res = await searchUsers(query); + const json = await res.json(); + return unwrapArray(json); + }, + enabled: enabled && query.length > 0, + }); +} diff --git a/src/hooks/useJam.ts b/src/hooks/useJam.ts index d205caa..238e85b 100644 --- a/src/hooks/useJam.ts +++ b/src/hooks/useJam.ts @@ -1,8 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; -import { getCurrentJam } from "@/helpers/jam"; // Adjust this path if needed -import type { JamType, JamPhase } from "@/types/JamType"; // Adjust types +import { useCurrentJam } from "@/hooks/queries"; +import type { JamType, JamPhase } from "@/types/JamType"; type UseJamReturn = { jam: JamType | null; @@ -10,18 +9,10 @@ type UseJamReturn = { }; export function useJam(): UseJamReturn { - const [jam, setJam] = useState(null); - const [jamPhase, setJamPhase] = useState(null); + const { data } = useCurrentJam(); - useEffect(() => { - const fetchJam = async () => { - const jamResponse = await getCurrentJam(); - setJam(jamResponse?.jam || null); - setJamPhase(jamResponse?.phase || null); - }; - - fetchJam(); - }, []); - - return { jam, jamPhase }; + return { + jam: data?.jam ?? null, + jamPhase: data?.phase ?? null, + }; } diff --git a/src/providers/EmojiProvider.tsx b/src/providers/EmojiProvider.tsx index acf63d9..747a3a7 100644 --- a/src/providers/EmojiProvider.tsx +++ b/src/providers/EmojiProvider.tsx @@ -1,9 +1,11 @@ "use client"; -import { BASE_URL } from "@/requests/config"; -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { createContext, useContext, useMemo } from "react"; import type { ReactNode } from "react"; import type { ReactionType } from "@/types/ReactionType"; +import { useEmojisQuery } from "@/hooks/queries"; +import { useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "@/hooks/queries/queryKeys"; export type EmojiType = ReactionType; @@ -17,38 +19,19 @@ type EmojiContextValue = { const EmojiContext = createContext(undefined); export function EmojiProvider({ children }: { children: ReactNode }) { - const [emojis, setEmojis] = useState([]); - const [loading, setLoading] = useState(true); - - const loadEmojis = useCallback(async () => { - setLoading(true); - try { - const response = await fetch(`${BASE_URL}/emojis`); - const data = await response.json(); - setEmojis(Array.isArray(data?.data) ? data.data : []); - } catch (error) { - console.error("Failed to load emojis", error); - setEmojis([]); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - loadEmojis(); - }, [loadEmojis]); - - const emojiMap = useMemo(() => { - const map: Record = {}; - emojis.forEach((emoji) => { - map[emoji.slug] = emoji; - }); - return map; - }, [emojis]); + const { emojis, emojiMap, isLoading } = useEmojisQuery(); + const queryClient = useQueryClient(); + + const refresh = useMemo( + () => async () => { + await queryClient.invalidateQueries({ queryKey: queryKeys.emoji.list() }); + }, + [queryClient] + ); return ( {children} diff --git a/src/providers/SiteThemeProvider.tsx b/src/providers/SiteThemeProvider.tsx index c4f3dda..a644652 100644 --- a/src/providers/SiteThemeProvider.tsx +++ b/src/providers/SiteThemeProvider.tsx @@ -9,7 +9,7 @@ import { SiteThemeType } from "@/types/SiteThemeType"; import { createContext, useContext, useEffect, useState } from "react"; import Cookies from "js-cookie"; -import { BASE_URL } from "@/requests/config"; +import { useSiteThemes } from "@/hooks/queries"; type ColorsMap = SiteThemeType["colors"]; @@ -34,41 +34,26 @@ export function SiteThemeProvider({ children }: { children: React.ReactNode }) { }); const [previewedSiteTheme, setPreviewedSiteThemeBacking] = useState(null); - const [allSiteThemes, setAllSiteThemes] = useState([]); const [isThemeReady, setIsThemeReady] = useState(false); - async function readThemeFiles() { - const res = await fetch(`${BASE_URL}/site-themes`); - const { data } = await res.json(); - - return data; - } - - useEffect(() => { - async function loadThemes() { - const themes = await readThemeFiles(); - setAllSiteThemes(themes); - } - - loadThemes(); - }, []); + const { data: allSiteThemes = [] } = useSiteThemes(); useEffect(() => { if (allSiteThemes.length == 0) return; const currentTheme = Cookies.get("theme"); - const match = allSiteThemes.find((t) => t.name === currentTheme); + const match = allSiteThemes.find((t: SiteThemeType) => t.name === currentTheme); if (match) { setSiteThemeBacking(match); } else { - const defaultMatch = allSiteThemes.find((t) => t.name === "Obsidian"); + const defaultMatch = allSiteThemes.find((t: SiteThemeType) => t.name === "Obsidian"); setSiteThemeBacking(defaultMatch as SiteThemeType); } }, [allSiteThemes]); function setSiteTheme(name: string) { - const match = allSiteThemes.find((t) => t.name === name); + const match = allSiteThemes.find((t: SiteThemeType) => t.name === name); if (match) { Cookies.set("theme", match.name, { expires: 36500 }); setSiteThemeBacking(match); @@ -81,7 +66,7 @@ export function SiteThemeProvider({ children }: { children: React.ReactNode }) { return; } - const match = allSiteThemes.find((t) => t.name === name); + const match = allSiteThemes.find((t: SiteThemeType) => t.name === name); if (match) { setPreviewedSiteThemeBacking(match); } @@ -105,7 +90,7 @@ export function SiteThemeProvider({ children }: { children: React.ReactNode }) { value={{ siteTheme: effectiveTheme, colors: effectiveTheme.colors, - allSiteThemes: allSiteThemes.filter((theme) => !theme.hidden), + allSiteThemes: allSiteThemes.filter((theme: SiteThemeType) => !theme.hidden), setSiteTheme, setPreviewedSiteTheme, }} diff --git a/src/requests/cache.ts b/src/requests/cache.ts deleted file mode 100644 index c62c784..0000000 --- a/src/requests/cache.ts +++ /dev/null @@ -1,113 +0,0 @@ -type CacheEntry = { - expiresAt: number; - response?: Response; - promise?: Promise; -}; - -type CachedFetchOptions = { - ttlMs?: number; -}; - -const DEFAULT_TTL_MS = 30_000; -const requestCache = new Map(); - -function normalizeHeaders(headers?: HeadersInit): string { - if (!headers) return ""; - - if (headers instanceof Headers) { - return Array.from(headers.entries()) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => `${key}:${value}`) - .join("|"); - } - - if (Array.isArray(headers)) { - return [...headers] - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => `${key}:${value}`) - .join("|"); - } - - return Object.entries(headers) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => `${key}:${value}`) - .join("|"); -} - -function getCacheKey(input: RequestInfo | URL, init?: RequestInit): string { - const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - const method = init?.method ?? "GET"; - const credentials = init?.credentials ?? ""; - const headers = normalizeHeaders(init?.headers); - - return [method, url, credentials, headers].join("::"); -} - -export async function cachedFetch( - input: RequestInfo | URL, - init?: RequestInit, - options?: CachedFetchOptions, -): Promise { - const method = (init?.method ?? "GET").toUpperCase(); - if (method !== "GET") { - return fetch(input, init); - } - - const now = Date.now(); - const ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS; - const key = getCacheKey(input, init); - const cached = requestCache.get(key); - - if (cached && cached.response && cached.expiresAt > now) { - return cached.response.clone(); - } - - if (cached?.promise) { - const response = await cached.promise; - return response.clone(); - } - - const promise = fetch(input, init).then((response) => { - if (response.ok) { - requestCache.set(key, { - expiresAt: Date.now() + ttlMs, - response: response.clone(), - }); - } else { - requestCache.delete(key); - } - - return response; - }).catch((error) => { - requestCache.delete(key); - throw error; - }); - - requestCache.set(key, { - expiresAt: now + ttlMs, - promise, - }); - - const response = await promise; - return response.clone(); -} - -export function invalidateRequestCache(match?: string | RegExp | ((key: string) => boolean)) { - if (!match) { - requestCache.clear(); - return; - } - - for (const key of requestCache.keys()) { - const shouldDelete = - typeof match === "string" - ? key.includes(match) - : match instanceof RegExp - ? match.test(key) - : match(key); - - if (shouldDelete) { - requestCache.delete(key); - } - } -} diff --git a/src/requests/event.ts b/src/requests/event.ts index 13193bc..9425085 100644 --- a/src/requests/event.ts +++ b/src/requests/event.ts @@ -1,17 +1,12 @@ import { getCookie } from "@/helpers/cookie"; import { BASE_URL } from "./config"; -import { cachedFetch, invalidateRequestCache } from "./cache"; export async function getEvents(filter: string) { - return cachedFetch(`${BASE_URL}/events?filter=${filter}`, undefined, { - ttlMs: 20_000, - }); + return fetch(`${BASE_URL}/events?filter=${filter}`); } export async function getEvent(eventSlug: string) { - return cachedFetch(`${BASE_URL}/event?targetEventSlug=${eventSlug}`, undefined, { - ttlMs: 20_000, - }); + return fetch(`${BASE_URL}/event?targetEventSlug=${eventSlug}`); } export async function postEvent( @@ -40,9 +35,5 @@ export async function postEvent( credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/event|\/events|\/self|\/user/); - } - return response; } diff --git a/src/requests/game.ts b/src/requests/game.ts index a58c701..8342246 100644 --- a/src/requests/game.ts +++ b/src/requests/game.ts @@ -4,41 +4,32 @@ import { PlatformType } from "@/types/DownloadLinkType"; import { AchievementType } from "@/types/AchievementType"; import { LeaderboardType } from "@/types/LeaderboardType"; import { GameEmbedAspectRatio } from "@/types/GameType"; -import { cachedFetch, invalidateRequestCache } from "./cache"; export async function getCurrentGame() { - return cachedFetch( - `${BASE_URL}/self/current-game?username=${getCookie("user")}`, - { - headers: { authorization: `Bearer ${getCookie("token")}` }, - credentials: "include", - }, - { ttlMs: 15_000 }, - ); + return fetch(`${BASE_URL}/self/current-game?username=${getCookie("user")}`, { + headers: { authorization: `Bearer ${getCookie("token")}` }, + credentials: "include", + }); } export async function getRatingCategories(always: boolean = false) { - return cachedFetch( + return fetch( `${BASE_URL}/rating-categories?always=${always ? "true" : "false"}`, - undefined, - { ttlMs: 300_000 }, ); } export async function getFlags() { - return cachedFetch(`${BASE_URL}/flags`, undefined, { ttlMs: 300_000 }); + return fetch(`${BASE_URL}/flags`); } export async function getGameTags() { - return cachedFetch(`${BASE_URL}/gametags`, undefined, { ttlMs: 300_000 }); + return fetch(`${BASE_URL}/gametags`); } export async function getGame(gameSlug: string) { - return cachedFetch(`${BASE_URL}/games/${gameSlug}`, { + return fetch(`${BASE_URL}/games/${gameSlug}`, { headers: { authorization: `Bearer ${getCookie("token")}` }, credentials: "include", - }, { - ttlMs: 30_000, }); } @@ -91,7 +82,7 @@ export async function postGame( estOneRun: string | null, estAnyPercent: string | null, estHundredPercent: string | null, - emotePrefix: string | null + emotePrefix: string | null, ) { const response = await fetch(`${BASE_URL}/game`, { body: JSON.stringify({ @@ -132,10 +123,6 @@ export async function postGame( credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/(games|game|self|user|jam|results)/); - } - return response; } @@ -188,7 +175,7 @@ export async function updateGame( estOneRun: string | null, estAnyPercent: string | null, estHundredPercent: string | null, - emotePrefix: string | null + emotePrefix: string | null, ) { const response = await fetch(`${BASE_URL}/games/${previousGameSlug}`, { body: JSON.stringify({ @@ -228,10 +215,6 @@ export async function updateGame( credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/(games|game|self|user|jam|results)/); - } - return response; } @@ -241,9 +224,7 @@ export async function getGames(sort: string, jamId?: string) { params.set("jamId", jamId); } - return cachedFetch(`${BASE_URL}/games?${params.toString()}`, undefined, { - ttlMs: 20_000, - }); + return fetch(`${BASE_URL}/games?${params.toString()}`); } export async function getResults( @@ -268,16 +249,10 @@ export async function getResults( params.set("preview", "1"); } - return cachedFetch( - `${BASE_URL}/results?${params.toString()}`, - { - credentials: "include", - headers: { - authorization: `Bearer ${getCookie("token")}`, - }, - }, - { - ttlMs: 20_000, + return fetch(`${BASE_URL}/results?${params.toString()}`, { + credentials: "include", + headers: { + authorization: `Bearer ${getCookie("token")}`, }, - ); + }); } diff --git a/src/requests/jam.ts b/src/requests/jam.ts index b5bfd69..08ba758 100644 --- a/src/requests/jam.ts +++ b/src/requests/jam.ts @@ -1,18 +1,13 @@ import { getCookie } from "@/helpers/cookie"; import { BASE_URL } from "./config"; -import { cachedFetch, invalidateRequestCache } from "./cache"; export async function getJams() { - return cachedFetch(`${BASE_URL}/jams`, undefined, { - ttlMs: 300_000, - }); + return fetch(`${BASE_URL}/jams`); } export async function getCurrentJam() { - return cachedFetch(`${BASE_URL}/jam`, { + return fetch(`${BASE_URL}/jam`, { next: { revalidate: 300 }, - }, { - ttlMs: 60_000, }); } @@ -30,20 +25,14 @@ export async function joinJam(jamId: number) { }, }); - if (response.ok) { - invalidateRequestCache(/\/(jam|jams|self|user)/); - } - return response; } export async function hasJoinedCurrentJam() { - return cachedFetch(`${BASE_URL}/jam/participation`, { + return fetch(`${BASE_URL}/jam/participation`, { credentials: "include", headers: { Authorization: `Bearer ${getCookie("token")}`, }, - }, { - ttlMs: 15_000, }); } diff --git a/src/requests/post.ts b/src/requests/post.ts index 8527fa1..24b1138 100644 --- a/src/requests/post.ts +++ b/src/requests/post.ts @@ -1,7 +1,6 @@ import { getCookie } from "@/helpers/cookie"; import { BASE_URL } from "./config"; import { PostTime } from "@/types/PostTimes"; -import { cachedFetch, invalidateRequestCache } from "./cache"; export async function getPosts( sort: string, @@ -21,13 +20,13 @@ export async function getPosts( .join("_")}`; } - return cachedFetch(url, undefined, { ttlMs: 20_000 }); + return fetch(url); } export async function getPost(postSlug: string, userSlug?: string) { let url = `${BASE_URL}/post?slug=${postSlug}`; if (userSlug) url += `&user=${userSlug}`; - return cachedFetch(url, undefined, { ttlMs: 20_000 }); + return fetch(url); } export async function postPost( @@ -52,10 +51,6 @@ export async function postPost( credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/post|\/posts|\/self|\/user/); - } - return response; } @@ -74,10 +69,6 @@ export async function deletePost(postId: number) { credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/post|\/posts|\/self|\/user/); - } - return response; } @@ -96,10 +87,6 @@ export async function removePost(postId: number) { credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/post|\/posts|\/self|\/user/); - } - return response; } @@ -118,10 +105,6 @@ export async function stickPost(postId: number, sticky: boolean) { credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/post|\/posts|\/self|\/user/); - } - return response; } @@ -147,10 +130,6 @@ export async function updatePost( credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/post|\/posts|\/self|\/user/); - } - return response; } @@ -168,9 +147,5 @@ export async function togglePostReaction(postId: number, reactionId: number) { }), }); - if (response.ok) { - invalidateRequestCache(/\/post|\/posts|\/self|\/user/); - } - return response; } diff --git a/src/requests/track.ts b/src/requests/track.ts index 6749323..7e0a750 100644 --- a/src/requests/track.ts +++ b/src/requests/track.ts @@ -1,13 +1,10 @@ import { getCookie } from "@/helpers/cookie"; import { BASE_URL } from "./config"; -import { cachedFetch, invalidateRequestCache } from "./cache"; export async function getTrack(trackSlug: string) { - return cachedFetch(`${BASE_URL}/tracks/${trackSlug}`, { + return fetch(`${BASE_URL}/tracks/${trackSlug}`, { headers: { authorization: `Bearer ${getCookie("token")}` }, credentials: "include", - }, { - ttlMs: 30_000, }); } @@ -17,9 +14,7 @@ export async function getTracks(sort: string, jamId?: string) { params.set("jamId", jamId); } - return cachedFetch(`${BASE_URL}/tracks?${params.toString()}`, undefined, { - ttlMs: 20_000, - }); + return fetch(`${BASE_URL}/tracks?${params.toString()}`); } export async function updateTrack( @@ -51,28 +46,20 @@ export async function updateTrack( body: JSON.stringify(payload), }); - if (response.ok) { - invalidateRequestCache(/\/(tracks|results|self|user|jam)/); - } - return response; } export async function getTrackTags() { - return cachedFetch(`${BASE_URL}/tracktags`, { + return fetch(`${BASE_URL}/tracktags`, { headers: { authorization: `Bearer ${getCookie("token")}` }, credentials: "include", - }, { - ttlMs: 300_000, }); } export async function getTrackFlags() { - return cachedFetch(`${BASE_URL}/trackflags`, { + return fetch(`${BASE_URL}/trackflags`, { headers: { authorization: `Bearer ${getCookie("token")}` }, credentials: "include", - }, { - ttlMs: 300_000, }); } @@ -95,20 +82,16 @@ export async function getTrackResults( params.set("preview", "1"); } - return cachedFetch(`${BASE_URL}/results?${params.toString()}`, { + return fetch(`${BASE_URL}/results?${params.toString()}`, { headers: { authorization: `Bearer ${getCookie("token")}` }, credentials: "include", - }, { - ttlMs: 20_000, }); } export async function getTrackRatingCategories() { - return cachedFetch(`${BASE_URL}/track-rating-categories`, { + return fetch(`${BASE_URL}/track-rating-categories`, { headers: { authorization: `Bearer ${getCookie("token")}` }, credentials: "include", - }, { - ttlMs: 300_000, }); } diff --git a/src/requests/user.ts b/src/requests/user.ts index da72e3f..7bb221c 100644 --- a/src/requests/user.ts +++ b/src/requests/user.ts @@ -1,24 +1,19 @@ import { getCookie } from "@/helpers/cookie"; import { BASE_URL } from "./config"; -import { cachedFetch, invalidateRequestCache } from "./cache"; export async function getSelf() { const userCookie = getCookie("user"); const tokenCookie = getCookie("token"); //if (!userCookie || !tokenCookie) return Promise.reject("Cookie not found."); - return cachedFetch(`${BASE_URL}/self?username=${userCookie}`, { + return fetch(`${BASE_URL}/self?username=${userCookie}`, { headers: { authorization: `Bearer ${tokenCookie}` }, credentials: "include", - }, { - ttlMs: 15_000, }); } export async function getUser(userSlug: string) { - return cachedFetch(`${BASE_URL}/user?targetUserSlug=${userSlug}`, undefined, { - ttlMs: 30_000, - }); + return fetch(`${BASE_URL}/user?targetUserSlug=${userSlug}`); } export async function searchUsers(query: string) { @@ -89,9 +84,5 @@ export async function updateUser( credentials: "include", }); - if (response.ok) { - invalidateRequestCache(/\/(self|user|jam)/); - } - return response; } diff --git a/src/types/UserType.ts b/src/types/UserType.ts index 1d9fd68..74acbeb 100644 --- a/src/types/UserType.ts +++ b/src/types/UserType.ts @@ -1,5 +1,6 @@ import { AchievementType } from "./AchievementType"; import { CommentType } from "./CommentType"; +import { JamType } from "./JamType"; import { NotificationType } from "./NotificationType"; import { PostType } from "./PostType"; import { RoleType } from "./RoleType"; @@ -136,4 +137,6 @@ export interface UserType { posts: PostType[]; comments: CommentType[]; receivedNotifications: NotificationType[]; + jams?: JamType[]; + email?: string; }