Files
hypixel-stats/src/app/(stats)/guild/[value]/_components/members.tsx
2025-09-29 11:32:27 +02:00

277 lines
9.6 KiB
TypeScript

"use client"
import { Card, CardContent } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { getColor } from "@/lib/colors"
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 z from "zod"
type PlayerForGuild = z.infer<typeof playerForGuildSchema>
import Image from "next/image"
import Link from "next/link"
import { useEffect, useState } from "react"
import { toast } from "sonner"
type MemberWithPlayer = Guild["guild"]["members"][number] & {
player?: PlayerForGuild["player"]
loading?: boolean
error?: boolean
}
export function GuildMembers({ members: mem, ranks }: { members: Guild["guild"]["members"], ranks: Guild["guild"]["ranks"] }) {
const [members, setMembers] = useState<MemberWithPlayer[]>(
mem.map(member => ({ ...member, loading: false, error: false }))
)
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 {
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))
}
} catch {
setMembers(prev => prev.map((member, i) => i === index ? { ...member, loading: false, error: true } : member))
}
}
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 () => clearTimeout(timer)
}
}, [currentIndex, members, isLoading])
toast.info(`Loaded ${members.filter(member => member.player).length} out of ${members.length} guild members`, {
id: "guild.members.loader",
duration: 1000
})
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">
{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) => <MemberCard key={i} member={member} />)}
</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>
<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}
/>
</TableCell>
<TableCell>{m.rank}</TableCell>
<TableCell>{formatNumber(Object.values(m.expHistory).reduce((a, b) => a + b))}</TableCell>
<TableCell>{formatDate(m.joined)}</TableCell>
</TableRow>
)
}
function PlayerIGN(
{ ign, rank, monthly, rankColor, specialRank, prefix }: {
ign: string
rank: string | undefined
monthly: string | undefined
rankColor: string | undefined
specialRank: string | undefined
prefix: string | undefined
}
) {
if (prefix === "[PIG+++]") {
return <span className="text-mc-light-purple">{ign}</span>
}
if (specialRank) {
if (specialRank === "YOUTUBER") {
return <span className="text-mc-red">{ign}</span>
}
if (specialRank === "STAFF") {
return <span className="text-mc-red">{ign}</span>
}
}
if (monthly === "SUPERSTAR") {
if (rankColor === "GOLD") {
return <span className="text-mc-gold">{ign}</span>
} else {
return <span className="text-mc-aqua">{ign}</span>
}
}
switch (rank) {
case "VIP":
return <span className="text-mc-green">{ign}</span>
case "VIP_PLUS":
return <span className="text-mc-green">{ign}</span>
case "MVP":
return <span className="text-mc-aqua">{ign}</span>
case "MVP_PLUS":
return <span className="text-mc-aqua">{ign}</span>
default:
return <span className="text-mc-gray">{ign}</span>
}
}
function PlayerRank(
{ rank, monthly, plusColor, rankColor, specialRank, prefix }: {
rank: string | undefined
monthly: string | undefined
plusColor?: string
rankColor: string | undefined
specialRank: string | undefined
prefix: string | undefined
}
) {
if (prefix === "[PIG+++]") {
return (
<>
<span className="text-mc-light-purple">[PIG</span>
<span className="text-mc-aqua">+++</span>
<span className="text-mc-light-purple">]</span>
</>
)
}
if (specialRank) {
if (specialRank === "YOUTUBER") {
return (
<>
<span className="text-mc-red">[</span>
<span className="text-mc-white">YOUTUBE</span>
<span className="text-mc-red">]</span>
</>
)
}
if (specialRank === "STAFF") {
return (
<>
<span className="text-mc-red">[</span>
<span className="text-mc-gold"></span>
<span className="text-mc-red">]</span>
</>
)
}
}
if (monthly === "SUPERSTAR") {
if (rankColor === "GOLD") {
return (
<>
<span className="text-mc-gold">[MVP</span>
<span className={getColor(plusColor)}>++</span>
<span className="text-mc-gold">]</span>
</>
)
} else {
return (
<>
<span className="text-mc-aqua">[MVP</span>
<span className={getColor(plusColor)}>++</span>
<span className="text-mc-aqua">]</span>
</>
)
}
}
switch (rank) {
case "VIP":
return <span className="text-mc-green">[VIP]</span>
case "VIP_PLUS":
return (
<>
<span className="text-mc-green">[VIP</span>
<span className="text-mc-gold">+</span>
<span className="text-mc-green">]</span>
</>
)
case "MVP":
return <span className="text-mc-aqua">[MVP]</span>
case "MVP_PLUS":
return (
<>
<span className="text-mc-aqua">[MVP</span>
<span className={getColor(plusColor)}>+</span>
<span className="text-mc-aqua">]</span>
</>
)
default:
return null
}
}