Updated guild members
This commit is contained in:
20
src/app/(stats)/api/guildmembers/route.ts
Normal file
20
src/app/(stats)/api/guildmembers/route.ts
Normal 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 })
|
||||||
|
}
|
||||||
257
src/app/(stats)/guild/[value]/_components/members.tsx
Normal file
257
src/app/(stats)/guild/[value]/_components/members.tsx
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -172,6 +172,7 @@
|
|||||||
|
|
||||||
* {
|
* {
|
||||||
@apply transition-colors duration-200;
|
@apply transition-colors duration-200;
|
||||||
|
@apply selection:bg-primary selection:text-primary-foreground
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"]>
|
||||||
|
|||||||
Reference in New Issue
Block a user