Files
hypixel-stats/src/app/(stats)/guild/[value]/_components/members.tsx
2025-10-29 17:26:20 +01:00

165 lines
6.3 KiB
TypeScript

"use client"
import { PlayerIGN, PlayerRank } from "@/app/(stats)/_components/displayname"
import { Card, CardContent } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
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 { ApiResponse } from "@/types"
import { useQueries } from "@tanstack/react-query"
import Image from "next/image"
import Link from "next/link"
import { useEffect, useRef } from "react"
import { toast } from "sonner"
import z from "zod"
type PlayerForGuild = z.infer<typeof playerForGuildSchema>
type MemberWithPlayer = Guild["guild"]["members"][number] & {
player?: PlayerForGuild["player"]
loading?: boolean
error?: boolean
}
export function GuildMembers({ members, ranks }: { members: Guild["guild"]["members"], ranks: Guild["guild"]["ranks"] }) {
const hasShownToast = useRef(false)
const totalMembers = members.length
const TOAST_ID = "guild.members.progress"
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
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)
})
useEffect(() => {
if (!hasShownToast.current && totalMembers > 0) {
toast.loading(`Loading guild members... (0/${totalMembers})`, {
id: TOAST_ID,
duration: 1000
})
hasShownToast.current = true
}
}, [totalMembers, TOAST_ID])
useEffect(() => {
if (loadedCount > 0) {
if (loadedCount >= totalMembers) {
toast.success(`Loaded all ${totalMembers} guild members!`, {
id: TOAST_ID
})
} else {
toast.loading(`Loading guild members... (${loadedCount}/${totalMembers})`, {
id: TOAST_ID
})
}
}
}, [loadedCount, totalMembers, TOAST_ID])
return (
<Card>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>Name</TableHead>
<TableHead>Rank</TableHead>
<TableHead>Weekly GEXP</TableHead>
<TableHead>Joined Since</TableHead>
</TableRow>
</TableHeader>
<TableBody className="space-y-4">
{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>
</Card>
)
}
function MemberCard({ member: m }: { member: MemberWithPlayer }) {
return (
<TableRow>
<TableCell>
<Link href={`https://namemc.com/profile/${m.uuid}`}>
<Image
src={head(m.uuid, 32)}
width={32}
height={32}
alt={`${m.player?.displayname}'s head`}
unoptimized
className="shadow-2xl"
/>
</Link>
</TableCell>
<TableCell>
<Link href={`/player/${m.player?.displayname}`} prefetch={false}>
<PlayerRank
rank={m.player?.newPackageRank}
monthly={m.player?.monthlyPackageRank}
rankColor={m.player?.monthlyRankColor}
specialRank={m.player?.rank}
prefix={m.player?.prefix}
plusColor={m.player?.rankPlusColor}
/>{" "}
<PlayerIGN
ign={m.player!.displayname}
rank={m.player?.newPackageRank}
monthly={m.player?.monthlyPackageRank}
rankColor={m.player?.monthlyRankColor}
specialRank={m.player?.rank}
prefix={m.player?.prefix}
/>
</Link>
</TableCell>
<TableCell>{m.rank}</TableCell>
<TableCell>{formatNumber(Object.values(m.expHistory).reduce((a, b) => a + b))}</TableCell>
<TableCell>{formatDate(m.joined)}</TableCell>
</TableRow>
)
}