165 lines
6.3 KiB
TypeScript
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>
|
|
)
|
|
}
|