From 781338a81bbfc966d6ea5655f465b4311ffd2bbf Mon Sep 17 00:00:00 2001 From: Taken Date: Sat, 13 Sep 2025 23:28:47 +0200 Subject: [PATCH] Added online status to sidebar --- .../player/[ign]/_components/Sidebar.tsx | 62 ++++- src/app/(stats)/player/[ign]/page.tsx | 3 + src/data/hypixel/hypixel.ts | 249 ++++++++++++++++++ src/lib/hypixel/api/online.ts | 31 +++ src/lib/hypixel/general/status.ts | 20 ++ src/lib/schema/status.ts | 17 ++ đ | 18 ++ 7 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 src/data/hypixel/hypixel.ts create mode 100644 src/lib/hypixel/api/online.ts create mode 100644 src/lib/hypixel/general/status.ts create mode 100644 src/lib/schema/status.ts create mode 100644 đ diff --git a/src/app/(stats)/player/[ign]/_components/Sidebar.tsx b/src/app/(stats)/player/[ign]/_components/Sidebar.tsx index d51fb5c..bb452ff 100644 --- a/src/app/(stats)/player/[ign]/_components/Sidebar.tsx +++ b/src/app/(stats)/player/[ign]/_components/Sidebar.tsx @@ -4,9 +4,11 @@ import { Separator } from "@/components/ui/separator" import { getColor } from "@/lib/colors" import { formatDate, formatNumber } from "@/lib/formatters" import { getCoinMultiplier, getTotalChallenges, getTotalCoins, getTotalQuests, rewardClaimed } from "@/lib/hypixel/general/stats" +import { getGame, getGameMode } from "@/lib/hypixel/general/status" import { getGuildMember, getGuildRankTag, getMemberGEXP, getMemberWeeklyGEXP } from "@/lib/hypixel/guild/guild" import { Guild } from "@/lib/schema/guild" import { Player } from "@/lib/schema/player" +import { Session } from "@/lib/schema/status" import Link from "next/link" import SocialIcons from "./SocialIcons" @@ -18,9 +20,10 @@ type SidebarProps = { rank: string | undefined specialRank: string | undefined eulaCoins: boolean | undefined + session: Session["session"] | null } -export default function Sidebar({ level, ign, player, guild, rank, specialRank, eulaCoins }: SidebarProps) { +export default function Sidebar({ level, ign, player, guild, rank, specialRank, eulaCoins, session }: SidebarProps) { const levelMultiplier = getCoinMultiplier(level, rank, specialRank, eulaCoins) const levelMultiplierVal = levelMultiplier.value @@ -200,6 +203,62 @@ export default function Sidebar({ level, ign, player, guild, rank, specialRank, ) } + function OnlineStatus() { + if (!session?.online) return null + + const noMapGameTypes = ["BUILD_BATTLE", "HOUSING", "REPLAY"] + const noMapArcadeModes = [ + "DAYONE", + "DEFENDER", + "DRAGONWARS2", + "DROPPER", + "SOCCER", + "STARWARS", + "SIMON_SAYS", + "PARTY", + "DRAW_THEIR_THING", + "PIXEL_PARTY", + "THROW_OUT" + ] + + const showMode = !(session.gameType === "REPLAY") + const showMap = session.map !== undefined && + session.gameType !== undefined && + !noMapGameTypes.includes(session.gameType) && + session.mode !== undefined && + !(session.gameType === "ARCADE" && noMapArcadeModes.includes(session.mode)) + const mode = session.mode === "LOBBY" ? "Lobby" : getGameMode(session.gameType, session.mode) + + return ( + <> +
+

Online Status

+
+ {session.gameType && ( +

+ {"Game: "} + {getGame(session.gameType)?.name || "Unknown"} +

+ )} + {showMode && ( +

+ {"Mode: "} + {mode || "Unknown"} +

+ )} + {showMap && ( +

+ {"Map: "} + {session.map} +

+ )} +
+
+ + + ) + } + function SoicalLinks() { return (
@@ -235,6 +294,7 @@ export default function Sidebar({ level, ign, player, guild, rank, specialRank, + diff --git a/src/app/(stats)/player/[ign]/page.tsx b/src/app/(stats)/player/[ign]/page.tsx index 0498823..a9d1871 100644 --- a/src/app/(stats)/player/[ign]/page.tsx +++ b/src/app/(stats)/player/[ign]/page.tsx @@ -4,6 +4,7 @@ import { Card, CardContent } from "@/components/ui/card" import { env } from "@/lib/env/server" import { getGuild } from "@/lib/hypixel/api/guild" import { getUuid } from "@/lib/hypixel/api/mojang" +import { getSession } from "@/lib/hypixel/api/online" import { getPlayer } from "@/lib/hypixel/api/player" import { getExactLevel } from "@/lib/hypixel/general/level" import { Loader2Icon, ShieldAlert } from "lucide-react" @@ -78,6 +79,7 @@ async function SuspendedPage({ params }: Pick, "param ) } + const session = await getSession(mc.id) const guild = await getGuild(mc.id) const level = getExactLevel(player.networkExp) @@ -109,6 +111,7 @@ async function SuspendedPage({ params }: Pick, "param rank={player.newPackageRank} specialRank={player.rank} eulaCoins={player.eulaCoins} + session={session} /> {player.stats !== undefined ? ( diff --git a/src/data/hypixel/hypixel.ts b/src/data/hypixel/hypixel.ts new file mode 100644 index 0000000..50e1d72 --- /dev/null +++ b/src/data/hypixel/hypixel.ts @@ -0,0 +1,249 @@ +export const GAMES = [ + { id: "ARCADE", name: "Arcade Games" }, + { id: "ARENA", name: "Arena Brawl" }, + { id: "BATTLEGROUND", name: "Warlords" }, + { id: "BEDWARS", name: "Bed Wars" }, + { id: "BUILD_BATTLE", name: "Build Battle" }, + { id: "DUELS", name: "Duels" }, + { id: "GINGERBREAD", name: "Turbo Kart Racers" }, + { id: "HOUSING", name: "Housing" }, + { id: "LEGACY", name: "Classic Games" }, + { id: "MAIN", name: "Main" }, + { id: "MCGO", name: "Cops and Crims" }, + { id: "MURDER_MYSTERY", name: "Murder Mystery" }, + { id: "PAINTBALL", name: "Paintball" }, + { id: "PIT", name: "Pit" }, + { id: "PROTOTYPE", name: "Prototype" }, + { id: "QUAKECRAFT", name: "Quakecraft" }, + { id: "REPLAY", name: "Replay" }, + { id: "SKYBLOCK", name: "SkyBlock" }, + { id: "SKYWARS", name: "SkyWars" }, + { id: "SMP", name: "SMP" }, + { id: "SPEED_UHC", name: "Speed UHC" }, + { id: "SUPER_SMASH", name: "Smash Heroes" }, + { id: "SURVIVAL_GAMES", name: "Blitz SG" }, + { id: "TNTGAMES", name: "TNT Games" }, + { id: "TOURNAMENT", name: "Tournament Hall" }, + { id: "UHC", name: "UHC Champions" }, + { id: "VAMPIREZ", name: "VampireZ" }, + { id: "WALLS", name: "Walls" }, + { id: "WALLS3", name: "Mega Walls" }, + { id: "WOOL_GAMES", name: "Wool Games" } +] + +export const MODES = { + ARCADE: { + DAYONE: "Blocking Dead", + ONEINTHEQUIVER: "Bounty Hunters", + PVP_CTW: "Capture the Wool", + DEFENDER: "Creeper Attack", + DRAGONWARS2: "Dragon Wars", + DROPPER: "Dropper", + ENDER: "Ender Spleef", + FARM_HUNT: "Farm Hunt", + SOCCER: "Football", + STARWARS: "Galaxy Wars", + HALLOWEEN_SIMULATOR: "Halloween Simulator", + HIDE_AND_SEEK_PROP_HUNT: "Hide and Seek (Prop Hunt)", + HIDE_AND_SEEK_PARTY_POOPER: "Hide and Seek (Party Pooper)", + HOLE_IN_THE_WALL: "Hole in the Wall", + SIMON_SAYS: "Hypixel Says", + MINI_WALLS: "Mini Walls", + PARTY: "Party Games", + DRAW_THEIR_THING: "Pixel Painters", + PIXEL_PARTY: "Pixel Party", + THROW_OUT: "Throw Out", + ZOMBIES_DEAD_END: "Zombies", + ZOMBIES_BAD_BLOOD: "Zombies", + ZOMBIES_ALIEN_ARCADIUM: "Zombies", + ZOMBIES_PRISON: "Zombies" + }, + ARENA: { + "1v1": "1v1", + "2v2": "2v2", + "4v4": "4v4" + }, + BATTLEGROUND: { + mini_ctf: "Capture the Flag", + domination: "Domination", + team_deathmatch: "Team Deathmatch" + }, + BEDWARS: { + BEDWARS_EIGHT_ONE: "Solo", + BEDWARS_EIGHT_TWO: "Doubles", + BEDWARS_EIGHT_TWO_TOURNEY: "Doubles (Tournament)", + BEDWARS_FOUR_THREE: "3v3v3v3", + BEDWARS_FOUR_FOUR: "4v4v4v4", + BEDWARS_FOUR_FOUR_TOURNEY: "4v4v4v4 (Tournament)", + BEDWARS_TWO_FOUR: "4v4", + BEDWARS_TWO_FOUR_TOURNEY: "4v4 (Tournament)", + BEDWARS_EIGHT_ONE_RUSH: "Rush Solo", + BEDWARS_EIGHT_TWO_RUSH: "Rush Doubles", + BEDWARS_FOUR_FOUR_RUSH: "Rush 4v4v4v4", + BEDWARS_EIGHT_ONE_ULTIMATE: "Ultimate Solo", + BEDWARS_EIGHT_TWO_ULTIMATE: "Ultimate Doubles", + BEDWARS_FOUR_FOUR_ULTIMATE: "Ultimate 4v4v4v4", + BEDWARS_EIGHT_TWO_VOIDLESS: "Voidless Doubles", + BEDWARS_FOUR_FOUR_VOIDLESS: "Voidless 4v4v4v4", + BEDWARS_EIGHT_TWO_ARMED: "Armed Doubles", + BEDWARS_FOUR_FOUR_ARMED: "Armed 4v4v4v4", + BEDWARS_EIGHT_TWO_SWAP: "Swappage Doubles", + BEDWARS_FOUR_FOUR_SWAP: "Swappage 4v4v4v4", + BEDWARS_EIGHT_TWO_UNDERWORLD: "Underworld Doubles", + BEDWARS_FOUR_FOUR_UNDERWORLD: "Underworld 4v4v4v4", + BEDWARS_CASTLE: "Castle", + BEDWARS_PRACTICE: "Practice" + }, + BUILD_BATTLE: { + BUILD_BATTLE_SOLO_PRO: "Pro", + BUILD_BATTLE_SOLO_NORMAL: "Solo", + BUILD_BATTLE_TEAMS_NORMAL: "Teams", + BUILD_BATTLE_GUESS_THE_BUILD: "Guess the Build", + BUILD_BATTLE_HALLOWEEN: "Halloween Hyper", + BUILD_BATTLE_CHRISTMAS_NEW_TEAMS: "Holiday Teams", + BUILD_BATTLE_SOLO_NORMAL_LATEST: "Solo (1.14+)", + BUILD_BATTLE_CHRISTMAS_NEW_SOLO: "Holiday Solo", + BUILD_BATTLE_CHRISTMAS: "Christmas" + }, + DUELS: { + DUELS_UHC_DUEL: "UHC 1v1", + DUELS_UHC_DOUBLES: "UHC 2v2", + DUELS_UHC_TOURNAMENT: "UHC Championship", + DUELS_UHC_FOUR: "UHC 4v4", + DUELS_UHC_MEETUP: "UHC Deathmatch", + DUELS_OP_DUEL: "OP 1v1", + DUELS_OP_DOUBLES: "OP 2v2", + DUELS_SW_DUEL: "SkyWars 1v1", + DUELS_SW_DOUBLES: "SkyWars 2v2", + DUELS_SW_TOURNAMENT: "SkyWars Championship", + DUELS_BOW_DUEL: "Bow 1v1", + DUELS_BLITZ_DUEL: "Blitz 1v1", + DUELS_MW_DUEL: "MegaWalls 1v1", + DUELS_MW_DOUBLES: "MegaWalls 2v2", + DUELS_SUMO_DUEL: "Sumo 1v1", + DUELS_SUMO_TOURNAMENT: "Sumo Championship", + DUELS_BOWSPLEEF_DUEL: "Bow Spleef 1v1", + DUELS_PARKOUR_EIGHT: "Parkour", + DUELS_BOXING_DUEL: "Boxing 1v1", + DUELS_CLASSIC_DUEL: "Classic 1v1", + DUELS_POTION_DUEL: "NoDebuff 1v1", + DUELS_COMBO_DUEL: "Combo 1v1", + DUELS_BRIDGE_DUEL: "Bridge 1v1", + DUELS_BRIDGE_DOUBLES: "Bridge 2v2", + DUELS_BRIDGE_TOURNAMENT: "The Bridge Championship", + DUELS_BRIDGE_THREES: "Bridge 3v3", + DUELS_BRIDGE_FOUR: "Bridge 4v4", + DUELS_BRIDGE_2V2V2V2: "Bridge 2v2v2v2", + DUELS_BRIDGE_3V3V3V3: "Bridge 3v3v3v3", + DUELS_CAPTURE_THREES: "Bridge CTF 3v3", + DUELS_DUEL_ARENA: "Duel Arena" + }, + GINGERBREAD: {}, + HOUSING: {}, + LEGACY: {}, + MAIN: {}, + MCGO: { + normal: "Defusal", + gungame: "Gun Game", + deathmatch: "Team Deathmatch", + DEFUSAL_TOURNEY: "Defusal (Tournament)" + }, + MURDER_MYSTERY: { + MURDER_CLASSIC: "Classic", + MURDER_ASSASSINS: "Assassins", + MURDER_INFECTION: "Infection", + MURDER_DOUBLE_UP: "Double Up", + MURDER_HARDCORE: "Hardcore", + MURDER_SHOWDOWN: "Showdown" + }, + PAINTBALL: {}, + PIT: {}, + PROTOTYPE: {}, + QUAKECRAFT: { + teams: "Teams", + solo: "Solo", + solo_tourney: "Solo (Tournament)" + }, + SKYBLOCK: { + farming_1: "The Farming Islands", + crystal_hollows: "Crystal Hollows", + winter: "Jerry's Workshop", + foraging_1: "The Park", + dark_auction: "Dark Auction", + dungeon: "Dungeons", + combat_3: "The End", + crismon_isle: "Crimson Isle", + hub: "Hub", + instanced: "Kuudra's Hollow", + dynamic: "Private Island", + mining_3: "Dwarven Mines", + garden: "The Garden", + mining_1: "Gold Mine", + combat_2: "Blazing Fortress", + mining_2: "Deep Caverns", + combat_1: "Spider's Den" + }, + SKYWARS: { + ranked_normal: "Ranked", + solo_normal: "Solo Normal", + solo_insane: "Solo Insane", + team_normal: "Teams Normal", + team_insane: "Teams Insane", + mega_normal: "Mega", + mega_doubles: "Mega Doubles", + solo_insane_lucky: "Solo Lucky Block", + teams_normal_tourney: "Doubles Normal (Tournament)", + solo_insane_slime: "Solo Slime", + teams_insane_slime: "Teams Slime", + teams_insane_rush: "Teams Rush", + teams_insane_lucky: "Teams Lucky Block", + solo_crazyinsane: "Solo Crazy Insane (Tournament)", + teams_insane_tourney: "Doubles Insane (Tournament)", + solo_insane_hunters_vs_beasts: "Solo Hunters vs Beasts", + solo_insane_tnt_madness: "Solo TNT Madness", + solo_insane_rush: "Solo Rush", + teams_insane_tnt_madness: "Teams TNT Madness" + }, + SMP: {}, + SPEED_UHC: { + solo_normal: "Solo Normal", + solo_insane: "Solo Insane", + team_normal: "Teams Normal", + team_insane: "Teams Insane" + }, + SUPER_SMASH: { + solo_normal: "Solo", + teams_normal: "Teams", + friends_normal: "Friends" + }, + SURVIVAL_GAMES: { + solo_normal: "Solo", + teams_normal: "Teams" + }, + TNTGAMES: { + BOWSPLEEF: "Bow Spleef", + PVPRUN: "PVP Run", + TNTRUN: "TNT Run", + TNTAG: "TNT Tag", + CAPTURE: "Wizards", + TNTRUN_TOURNEY: "TNT Run Tourney", + TEAMS_NORMAL: "Teams Mode" + }, + TOURNAMENT: {}, + UHC: { + SOLO: "Solo", + TEAMS: "Teams" + }, + VAMPIREZ: {}, + WALLS: {}, + WALLS3: { + face_off: "Face Off", + standard: "Standard" + }, + WOOL_GAMES: { + wool_wars_two_four_tourney: "4v4 (Tournament)", + wool_wars_two_four: "4v4", + capture_the_wool_two_twenty: "Capture the Wool", + sheep_wars_two_six: "Sheep Wars" + } +} diff --git a/src/lib/hypixel/api/online.ts b/src/lib/hypixel/api/online.ts new file mode 100644 index 0000000..0b404ca --- /dev/null +++ b/src/lib/hypixel/api/online.ts @@ -0,0 +1,31 @@ +import { statusSchema } from "@/lib/schema/status" +import { cacheLife } from "next/dist/server/use-cache/cache-life" +import { env } from "../../env/server" + +const playerApi = "https://api.hypixel.net/v2/status" + +export async function getSession(uuid: string) { + "use cache" + + if (process.env.NODE_ENV === "production") { + cacheLife({ + stale: 1000 * 60, + revalidate: 1000 * 60 * 5, + expire: 1000 * 60 * 5 + }) + } + + const res = await fetch(`${playerApi}?uuid=${uuid}`, { + headers: { + "API-Key": env.HYPIXEL_API_KEY + } + }) + + if (!res.ok) return null + + const { success, data } = statusSchema.safeParse(await res.json()) + + if (!success) return null + + return data.session +} diff --git a/src/lib/hypixel/general/status.ts b/src/lib/hypixel/general/status.ts new file mode 100644 index 0000000..82a297f --- /dev/null +++ b/src/lib/hypixel/general/status.ts @@ -0,0 +1,20 @@ +import { GAMES, MODES } from "@/data/hypixel/hypixel" + +export function getGame(val: string) { + const game = GAMES.find(g => g.id === val) + return game || null +} + +export function getGameMode(gameType?: string, mode?: string) { + if (!gameType || !mode) return null + const game = getGame(gameType) + if (!game) return null + + const modes = MODES as Record | Record> + + const gameModes = modes[gameType] + if (!gameModes) return null + + const modeName = gameModes[mode] + return modeName || null +} diff --git a/src/lib/schema/status.ts b/src/lib/schema/status.ts new file mode 100644 index 0000000..c823621 --- /dev/null +++ b/src/lib/schema/status.ts @@ -0,0 +1,17 @@ +import z from "zod" + +export const statusSchema = z.object({ + session: z.discriminatedUnion("online", [ + z.object({ + online: z.literal(false) + }), + z.object({ + online: z.literal(true), + gameType: z.string().optional(), + mode: z.string().optional(), + map: z.string().optional() + }) + ]) +}) + +export type Session = z.infer diff --git a/đ b/đ new file mode 100644 index 0000000..cdcfa6d --- /dev/null +++ b/đ @@ -0,0 +1,18 @@ +import { GAMES, MODES } from "@/data/hypixel/hypixel" + +export function getGame(val: string) { + const game = GAMES.find(g => g.id === val) + return game || null +} + +export function getGameMode(gameType?: string, mode?: string) { + if (!gameType || !mode) return null + const game = getGame(gameType) + if (!game) return null + + const gameModes = MODES[gameType as keyof typeof MODES] + if (!gameModes) return null + + const modeValue = gameModes[mode as keyof typeof gameModes] + return modeValue || null +}