Updated code

This commit is contained in:
2025-10-29 17:26:20 +01:00
parent 857ac578dc
commit b713008819
7 changed files with 76 additions and 71 deletions

View File

@@ -34,6 +34,7 @@
"@eslint/eslintrc": "^3",
"@next/eslint-plugin-next": "15.5.2",
"@tailwindcss/postcss": "^4",
"@tanstack/react-query-devtools": "^5.90.2",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "19.1.12",
@@ -287,8 +288,12 @@
"@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="],
"@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=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
"@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=="],

View File

@@ -43,6 +43,7 @@
"@eslint/eslintrc": "^3",
"@next/eslint-plugin-next": "15.5.2",
"@tailwindcss/postcss": "^4",
"@tanstack/react-query-devtools": "^5.90.2",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "19.1.12",

View File

@@ -7,10 +7,11 @@ 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 { ApiResponse } from "@/types"
import { useQueries } from "@tanstack/react-query"
import Image from "next/image"
import Link from "next/link"
import { useCallback, useEffect, useRef, useState } from "react"
import { useEffect, useRef } from "react"
import { toast } from "sonner"
import z from "zod"
@@ -21,68 +22,27 @@ type MemberWithPlayer = Guild["guild"]["members"][number] & {
error?: boolean
}
const queryClient = new QueryClient()
export function GuildMembers({ members, ranks }: { members: Guild["guild"]["members"], ranks: Guild["guild"]["ranks"] }) {
return (
<QueryClientProvider client={queryClient}>
<GuildMembersInternal members={members} ranks={ranks} />
</QueryClientProvider>
)
}
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) {
throw new Error(data.message || "Failed to fetch member data")
}
return data.player as PlayerForGuild["player"]
},
staleTime: 1000 * 60 * 60 * 24,
gcTime: 1000 * 60 * 60,
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000)
})
}
function MemberRow({ member, onLoad }: { member: Guild["guild"]["members"][number], onLoad: () => void }) {
const { data: player, isLoading, isError } = useMemberData(member.uuid)
useEffect(() => {
if (!isLoading && (player || isError)) {
onLoad()
}
}, [isLoading, player, isError, onLoad])
if (isLoading || (!player && !isError)) return null
if (isError || !player) return null
const memberWithPlayer: MemberWithPlayer = {
...member,
player,
loading: isLoading,
error: isError
}
return <MemberCard member={memberWithPlayer} />
}
function GuildMembersInternal({ members, ranks }: { members: Guild["guild"]["members"], ranks: Guild["guild"]["ranks"] }) {
const [loadedCount, setLoadedCount] = useState(0)
const hasShownToast = useRef(false)
const totalMembers = members.length
const TOAST_ID = "guild.members.progress"
const handleMemberLoad = useCallback(() => {
setLoadedCount(prev => prev + 1)
}, [])
const memberQueries = useQueries({
queries: members.map(member => ({
queryKey: ["guildMember", member.uuid],
queryFn: async () => {
const response = await fetch(`/api/guildmembers?uuid=${member.uuid}`)
const data = await response.json() as ApiResponse<PlayerForGuild["player"]>
if (data.error) {
throw new Error(data.message)
}
return data.player
},
staleTime: 1000 * 60 * 60 * 24
}))
})
const loadedCount = memberQueries.filter(query => !query.isLoading && (query.data || query.isError)).length
const sortedMembers = [...members].sort((a, b) => {
if (a.rank === "Guild Master" && b.rank !== "Guild Master") return -1
@@ -139,13 +99,21 @@ function GuildMembersInternal({ members, ranks }: { members: Guild["guild"]["mem
</TableRow>
</TableHeader>
<TableBody className="space-y-4">
{sortedMembers.map(member => (
<MemberRow
key={member.uuid}
member={member}
onLoad={handleMemberLoad}
/>
))}
{sortedMembers.map((member) => {
const query = memberQueries[members.findIndex(m => m.uuid === member.uuid)]
if (query.isLoading || (!query.data && !query.isError)) return null
if (query.isError || !query.data) return null
const memberWithPlayer: MemberWithPlayer = {
...member,
player: query.data,
loading: query.isLoading,
error: query.isError
}
return <MemberCard key={member.uuid} member={memberWithPlayer} />
})}
</TableBody>
</Table>
</CardContent>

View File

@@ -1,10 +1,11 @@
import "./globals.css"
import { GeistSans as geist } from "geist/font/sans"
import type { Metadata } from "next"
import { QueryClientProvider, ReactQueryDevtools } from "@/components/query-provider"
import ThemeProvider from "@/components/theme-provider"
import { Toaster } from "@/components/ui/sonner"
import type { Metadata } from "next"
export const metadata: Metadata = {
title: {
@@ -21,10 +22,13 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
{/* {process.env.NODE_ENV === "development" && <script src="https://unpkg.com/react-scan/dist/auto.global.js" />} */}
</head>
<body className="antialiased">
<QueryClientProvider>
<ThemeProvider>
{children}
<Toaster />
</ThemeProvider>
<ReactQueryDevtools />
</QueryClientProvider>
</body>
</html>
)

View File

@@ -0,0 +1,16 @@
"use client"
import { QueryClient, QueryClientProvider as OriginalQueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools as OriginalReactQueryDevtools } from "@tanstack/react-query-devtools"
import { ReactNode } from "react"
const queryClient = new QueryClient()
export function QueryClientProvider({ children }: { children: ReactNode }) {
return <OriginalQueryClientProvider client={queryClient}>{children}</OriginalQueryClientProvider>
}
export function ReactQueryDevtools() {
if (process.env.NODE_ENV !== "development") return null
return <OriginalReactQueryDevtools client={queryClient} />
}

View File

@@ -1,6 +1,10 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export async function wait(ms: number) {
return new Promise((res) => setTimeout(res, ms))
}
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

7
src/types.ts Normal file
View File

@@ -0,0 +1,7 @@
export type ApiResponse<T> = {
error: true
message: string
} | {
error: false
player: T
}