Updated guild members

This commit is contained in:
2025-09-28 00:06:40 +02:00
parent 4cddfb8104
commit 75aed0d3a9
7 changed files with 326 additions and 4 deletions

View File

@@ -0,0 +1,20 @@
import { getPlayerForGuild } from "@/lib/hypixel/api/player"
import { NextRequest, NextResponse } from "next/server"
import z from "zod"
const schema = z.object({
uuid: z.string().min(1)
})
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const uuid = searchParams.get("uuid")
const { success, data } = schema.safeParse({ uuid })
if (!success) return NextResponse.json({ error: true, message: "Invalid uuid" }, { status: 400 })
const player = await getPlayerForGuild(data.uuid)
if (!player) return NextResponse.json({ error: true, message: "Player not found" }, { status: 404 })
return NextResponse.json({ error: false, player })
}

View File

@@ -0,0 +1,257 @@
"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 { PlayerForGuild } from "@/lib/schema/player"
import Image from "next/image"
import Link from "next/link"
import { useEffect, useState } from "react"
type MemberWithPlayer = Guild["guild"]["members"][number] & {
player?: PlayerForGuild["player"]
loading?: boolean
error?: boolean
}
export function GuildMembers({ members: mem }: { members: Guild["guild"]["members"] }) {
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])
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).map((member, i) => <MemberCard key={i} member={member} />)}
</TableBody>
</Table>
{currentIndex < members.length && (
<div className="text-sm text-center text-muted-foreground">
Loading members... ({members.filter(member => member.player).length}/{members.length})
</div>
)}
</CardContent>
</Card>
)
}
function MemberCard({ member: m }: { member: MemberWithPlayer }) {
return (
<TableRow>
<TableCell>
<Link href={`https://namemc.com/profile/${m.uuid}`}>
<Image
src={head(m.uuid, 40)}
width={40}
height={40}
alt={"Member 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
}
}

View File

@@ -111,7 +111,7 @@ export default function Sidebar({ guild }: SidebarProps) {
return ( return (
<Card className="mx-auto w-full lg:mx-0 lg:w-1/4 max-w-120 md:max-w-3/10"> <Card className="mx-auto w-full lg:mx-0 lg:w-1/4 max-w-120 md:max-w-3/10">
<CardHeader className="flex justify-center"> <CardHeader className="flex justify-center">
<CardTitle className={cn("text-4xl", guild.tag && textColor)}> <CardTitle className={cn("text-4xl text-shadow-lg", guild.tag && textColor)}>
{guild.tag !== undefined ? `[${guild.tag}]` : "No Guild Tag"} {guild.tag !== undefined ? `[${guild.tag}]` : "No Guild Tag"}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>

View File

@@ -9,6 +9,7 @@ import { Metadata } from "next"
import { ReactNode, Suspense } from "react" import { ReactNode, Suspense } from "react"
import z from "zod" import z from "zod"
import { GuildPageLoadText } from "./_client" import { GuildPageLoadText } from "./_client"
import { GuildMembers } from "./_components/members"
import Sidebar from "./_components/sidebar" import Sidebar from "./_components/sidebar"
export async function generateMetadata({ params, searchParams }: PageProps<"/guild/[value]">): Promise<Metadata> { export async function generateMetadata({ params, searchParams }: PageProps<"/guild/[value]">): Promise<Metadata> {
@@ -111,8 +112,8 @@ async function SuspendedPage({ params, searchParams }: Pick<PageProps<"/guild/[v
</div> </div>
<div className="flex flex-col gap-6 px-6 pb-4 mt-8 w-full max-w-7xl md:flex-row"> <div className="flex flex-col gap-6 px-6 pb-4 mt-8 w-full max-w-7xl md:flex-row">
<Sidebar guild={guild} /> <Sidebar guild={guild} />
<div className="mx-auto w-full lg:mx-0 lg:w-3/4 max-w-120 md:max-w-7/10"> <div className="mx-auto space-y-4 w-full lg:mx-0 lg:w-3/4 max-w-120 md:max-w-7/10">
Hello <GuildMembers members={guild.members} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -172,6 +172,7 @@
* { * {
@apply transition-colors duration-200; @apply transition-colors duration-200;
@apply selection:bg-primary selection:text-primary-foreground
} }
body { body {

View File

@@ -1,9 +1,39 @@
import { cacheLife } from "next/dist/server/use-cache/cache-life" import { cacheLife } from "next/dist/server/use-cache/cache-life"
import { env } from "../../env/server" import { env } from "../../env/server"
import { playerSchema } from "../../schema/player" import { playerForGuildSchema, playerSchema } from "../../schema/player"
const playerApi = "https://api.hypixel.net/v2/player" const playerApi = "https://api.hypixel.net/v2/player"
export async function getPlayerForGuild(uuid: string) {
"use cache"
// if (process.env.NODE_ENV === "production") {
cacheLife({
stale: 1000 * 60 * 60,
revalidate: 1000 * 60 * 60 * 24 * 7,
expire: 1000 * 60 * 60 * 24 * 7
})
// }
const res = await fetch(`${playerApi}?uuid=${uuid}`, {
headers: {
"API-Key": env.HYPIXEL_API_KEY
}
})
if (!res.ok) return null
const { success, data, error } = playerForGuildSchema.safeParse(await res.json())
if (error) {
console.log(error)
}
if (!success) return null
return data.player
}
export async function getPlayer(uuid: string) { export async function getPlayer(uuid: string) {
"use cache" "use cache"

View File

@@ -24,6 +24,18 @@ import { uhcSchema } from "./stats/uhc"
import { warlordsStatsSchema } from "./stats/warlords" import { warlordsStatsSchema } from "./stats/warlords"
import { woolGamesStatsSchema } from "./stats/woolgames" import { woolGamesStatsSchema } from "./stats/woolgames"
export const playerForGuildSchema = z.object({
player: z.object({
displayname: z.string(),
newPackageRank: z.string().optional(),
monthlyPackageRank: z.string().optional(),
monthlyRankColor: z.string().optional(),
rankPlusColor: z.string().optional(),
rank: z.string().optional(),
prefix: z.string().transform(v => v.replaceAll(/§[a-z]/g, "")).optional()
})
})
export const playerSchema = z.looseObject({ export const playerSchema = z.looseObject({
player: z.looseObject({ player: z.looseObject({
displayname: z.string(), displayname: z.string(),
@@ -112,4 +124,5 @@ export const playerSchema = z.looseObject({
}) })
export type Player = z.infer<typeof playerSchema> export type Player = z.infer<typeof playerSchema>
export type PlayerForGuild = z.infer<typeof playerForGuildSchema>
export type NonNullStats = NonNullable<Player["player"]["stats"]> export type NonNullStats = NonNullable<Player["player"]["stats"]>