From 75aed0d3a98b024928996892cb3119c4752686fa Mon Sep 17 00:00:00 2001 From: Taken Date: Sun, 28 Sep 2025 00:06:40 +0200 Subject: [PATCH] Updated guild members --- src/app/(stats)/api/guildmembers/route.ts | 20 ++ .../guild/[value]/_components/members.tsx | 257 ++++++++++++++++++ .../guild/[value]/_components/sidebar.tsx | 2 +- src/app/(stats)/guild/[value]/page.tsx | 5 +- src/app/globals.css | 1 + src/lib/hypixel/api/player.ts | 32 ++- src/lib/schema/player.ts | 13 + 7 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 src/app/(stats)/api/guildmembers/route.ts create mode 100644 src/app/(stats)/guild/[value]/_components/members.tsx diff --git a/src/app/(stats)/api/guildmembers/route.ts b/src/app/(stats)/api/guildmembers/route.ts new file mode 100644 index 0000000..8c1fa53 --- /dev/null +++ b/src/app/(stats)/api/guildmembers/route.ts @@ -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 }) +} diff --git a/src/app/(stats)/guild/[value]/_components/members.tsx b/src/app/(stats)/guild/[value]/_components/members.tsx new file mode 100644 index 0000000..2d258e1 --- /dev/null +++ b/src/app/(stats)/guild/[value]/_components/members.tsx @@ -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( + 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 ( + + + + + + + Name + Rank + Weekly GEXP + Joined Since + + + + {members.filter(member => member.player).map((member, i) => )} + +
+ {currentIndex < members.length && ( +
+ Loading members... ({members.filter(member => member.player).length}/{members.length}) +
+ )} +
+
+ ) +} + +function MemberCard({ member: m }: { member: MemberWithPlayer }) { + return ( + + + + {"Member + + + + {" "} + + + {m.rank} + {formatNumber(Object.values(m.expHistory).reduce((a, b) => a + b))} + {formatDate(m.joined)} + + ) +} + +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 {ign} + } + + if (specialRank) { + if (specialRank === "YOUTUBER") { + return {ign} + } + + if (specialRank === "STAFF") { + return {ign} + } + } + + if (monthly === "SUPERSTAR") { + if (rankColor === "GOLD") { + return {ign} + } else { + return {ign} + } + } + + switch (rank) { + case "VIP": + return {ign} + case "VIP_PLUS": + return {ign} + case "MVP": + return {ign} + case "MVP_PLUS": + return {ign} + default: + return {ign} + } +} + +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 ( + <> + [PIG + +++ + ] + + ) + } + + if (specialRank) { + if (specialRank === "YOUTUBER") { + return ( + <> + [ + YOUTUBE + ] + + ) + } + + if (specialRank === "STAFF") { + return ( + <> + [ + + ] + + ) + } + } + + if (monthly === "SUPERSTAR") { + if (rankColor === "GOLD") { + return ( + <> + [MVP + ++ + ] + + ) + } else { + return ( + <> + [MVP + ++ + ] + + ) + } + } + + switch (rank) { + case "VIP": + return [VIP] + case "VIP_PLUS": + return ( + <> + [VIP + + + ] + + ) + case "MVP": + return [MVP] + case "MVP_PLUS": + return ( + <> + [MVP + + + ] + + ) + default: + return null + } +} diff --git a/src/app/(stats)/guild/[value]/_components/sidebar.tsx b/src/app/(stats)/guild/[value]/_components/sidebar.tsx index a5b3758..06e42b6 100644 --- a/src/app/(stats)/guild/[value]/_components/sidebar.tsx +++ b/src/app/(stats)/guild/[value]/_components/sidebar.tsx @@ -111,7 +111,7 @@ export default function Sidebar({ guild }: SidebarProps) { return ( - + {guild.tag !== undefined ? `[${guild.tag}]` : "No Guild Tag"} diff --git a/src/app/(stats)/guild/[value]/page.tsx b/src/app/(stats)/guild/[value]/page.tsx index 7959146..8921d71 100644 --- a/src/app/(stats)/guild/[value]/page.tsx +++ b/src/app/(stats)/guild/[value]/page.tsx @@ -9,6 +9,7 @@ import { Metadata } from "next" import { ReactNode, Suspense } from "react" import z from "zod" import { GuildPageLoadText } from "./_client" +import { GuildMembers } from "./_components/members" import Sidebar from "./_components/sidebar" export async function generateMetadata({ params, searchParams }: PageProps<"/guild/[value]">): Promise { @@ -111,8 +112,8 @@ async function SuspendedPage({ params, searchParams }: Pick
-
- Hello +
+
diff --git a/src/app/globals.css b/src/app/globals.css index 6cf1838..5f57e07 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -172,6 +172,7 @@ * { @apply transition-colors duration-200; + @apply selection:bg-primary selection:text-primary-foreground } body { diff --git a/src/lib/hypixel/api/player.ts b/src/lib/hypixel/api/player.ts index 1b9c0fc..8f11a66 100644 --- a/src/lib/hypixel/api/player.ts +++ b/src/lib/hypixel/api/player.ts @@ -1,9 +1,39 @@ import { cacheLife } from "next/dist/server/use-cache/cache-life" import { env } from "../../env/server" -import { playerSchema } from "../../schema/player" +import { playerForGuildSchema, playerSchema } from "../../schema/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) { "use cache" diff --git a/src/lib/schema/player.ts b/src/lib/schema/player.ts index 8629af5..95acde2 100644 --- a/src/lib/schema/player.ts +++ b/src/lib/schema/player.ts @@ -24,6 +24,18 @@ import { uhcSchema } from "./stats/uhc" import { warlordsStatsSchema } from "./stats/warlords" 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({ player: z.looseObject({ displayname: z.string(), @@ -112,4 +124,5 @@ export const playerSchema = z.looseObject({ }) export type Player = z.infer +export type PlayerForGuild = z.infer export type NonNullStats = NonNullable