diff --git a/bun.lock b/bun.lock index 3fe417d..b1b5436 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@t3-oss/env-nextjs": "^0.13.8", + "@tanstack/react-query": "^5.90.2", "babel-plugin-react-compiler": "^19.1.0-rc.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -284,6 +285,10 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.12", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "postcss": "^8.4.41", "tailwindcss": "4.1.12" } }, "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.2", "", { "dependencies": { "@tanstack/query-core": "5.90.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], diff --git a/package.json b/package.json index da10d37..f07f8be 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@t3-oss/env-nextjs": "^0.13.8", + "@tanstack/react-query": "^5.90.2", "babel-plugin-react-compiler": "^19.1.0-rc.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/app/(stats)/guild/[value]/_components/members.tsx b/src/app/(stats)/guild/[value]/_components/members.tsx index f391a3b..fd7cbff 100644 --- a/src/app/(stats)/guild/[value]/_components/members.tsx +++ b/src/app/(stats)/guild/[value]/_components/members.tsx @@ -7,10 +7,9 @@ import { formatDate, formatNumber } from "@/lib/formatters" import { head } from "@/lib/hypixel/general" import { Guild } from "@/lib/schema/guild" import { playerForGuildSchema } from "@/lib/schema/player" +import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query" import Image from "next/image" import Link from "next/link" -import { useEffect, useState } from "react" -import { toast } from "sonner" import z from "zod" type PlayerForGuild = z.infer @@ -20,48 +19,69 @@ type MemberWithPlayer = Guild["guild"]["members"][number] & { error?: boolean } -export function GuildMembers({ members: mem, ranks }: { members: Guild["guild"]["members"], ranks: Guild["guild"]["ranks"] }) { - const [members, setMembers] = useState( - mem.map(member => ({ ...member, loading: false, error: false })) +const queryClient = new QueryClient() + +export function GuildMembers({ members, ranks }: { members: Guild["guild"]["members"], ranks: Guild["guild"]["ranks"] }) { + return ( + + + ) - const [currentIndex, setCurrentIndex] = useState(0) - const [isLoading, setIsLoading] = useState(false) +} - const fetchMemberData = async (uuid: string, index: number) => { - setMembers(prev => prev.map((member, i) => i === index ? { ...member, loading: true } : member)) - - try { +function useMemberData(uuid: string) { + return useQuery({ + queryKey: ["guildMember", uuid], + queryFn: async () => { const response = await fetch(`/api/guildmembers?uuid=${uuid}`) const data = await response.json() if (data.error) { - setMembers(prev => prev.map((member, i) => i === index ? { ...member, loading: false, error: true } : member)) - } else { - setMembers(prev => prev.map((member, i) => i === index ? { ...member, loading: false, player: data.player } : member)) + throw new Error(data.message || "Failed to fetch member data") } - } catch { - setMembers(prev => prev.map((member, i) => i === index ? { ...member, loading: false, error: true } : member)) - } + + return data.player as PlayerForGuild["player"] + }, + staleTime: 24 * 60 * 60 * 1000, + gcTime: 24 * 60 * 60 * 1000, + retry: 3, + retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000) + }) +} + +function MemberRow({ member }: { member: Guild["guild"]["members"][number] }) { + const { data: player, isLoading, isError } = useMemberData(member.uuid) + + if (isLoading || (!player && !isError)) return null + if (isError || !player) return null + + const memberWithPlayer: MemberWithPlayer = { + ...member, + player, + loading: isLoading, + error: isError } - useEffect(() => { - if (currentIndex < members.length && !isLoading) { - const timer = setTimeout(() => { - setIsLoading(true) - fetchMemberData(members[currentIndex].uuid, currentIndex).then(() => { - setCurrentIndex(prev => prev + 1) - setIsLoading(false) - }) - }, 100) + return +} - toast.info(`Loaded ${members.filter(member => member.player).length} out of ${members.length} guild members`, { - id: "guild.members.loader", - duration: 1000 - }) +function GuildMembersInternal({ members, ranks }: { members: Guild["guild"]["members"], ranks: Guild["guild"]["ranks"] }) { + const sortedMembers = [...members].sort((a, b) => { + if (a.rank === "Guild Master" && b.rank !== "Guild Master") return -1 + if (b.rank === "Guild Master" && a.rank !== "Guild Master") return 1 - return () => clearTimeout(timer) + const aRank = ranks?.find(rank => rank.name === a.rank) + const bRank = ranks?.find(rank => rank.name === b.rank) + + const aPriority = aRank?.priority ?? Number.MIN_SAFE_INTEGER + const bPriority = bRank?.priority ?? Number.MIN_SAFE_INTEGER + + if (aPriority !== bPriority) { + return bPriority - aPriority } - }, [currentIndex, members, isLoading]) + + return a.uuid.localeCompare(b.uuid) + }) return ( @@ -77,22 +97,7 @@ export function GuildMembers({ members: mem, ranks }: { members: Guild["guild"][ - {members.filter(member => member.player).sort((a, b) => { - if (a.rank === "Guild Master" && b.rank !== "Guild Master") return -1 - if (b.rank === "Guild Master" && a.rank !== "Guild Master") return 1 - - const aRank = ranks?.find(rank => rank.name === a.rank) - const bRank = ranks?.find(rank => rank.name === b.rank) - - const aPriority = aRank?.priority ?? Number.MIN_SAFE_INTEGER - const bPriority = bRank?.priority ?? Number.MIN_SAFE_INTEGER - - if (aPriority !== bPriority) { - return bPriority - aPriority - } - - return a.uuid.localeCompare(b.uuid) - }).map((member, i) => )} + {sortedMembers.map(member => )} @@ -139,3 +144,4 @@ function MemberCard({ member: m }: { member: MemberWithPlayer }) { ) } +