Finished sidebar

This commit is contained in:
2025-08-16 23:39:11 +02:00
parent 1921efc76a
commit c79d06f272
35 changed files with 1307 additions and 9 deletions

10
src/lib/env/server.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
import { createEnv } from "@t3-oss/env-nextjs";
import z from "zod";
export const env = createEnv({
server: {
HYPIXEL_API_KEY: z.string().min(1),
},
experimental__runtimeEnv: true,
emptyStringAsUndefined: true,
})

20
src/lib/formatters.ts Normal file
View File

@@ -0,0 +1,20 @@
const numberFormatter = new Intl.NumberFormat(undefined, {
maximumFractionDigits: 2,
minimumFractionDigits: 0,
});
const dateFormatter = new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
export function formatNumber(num: number): string {
return numberFormatter.format(num);
}
export function formatDate(timestamp: number): string {
return dateFormatter.format(new Date(timestamp));
}

View File

@@ -0,0 +1,20 @@
import { env } from "../../env/server"
import { guildSchema } from "../../schema/guild"
const guildApi = "https://api.hypixel.net/v2/guild"
export async function getGuild(id: string, type: "id" | "player" | "name" = "player") {
const res = await fetch(`${guildApi}?${type}=${id}`, {
headers: {
"API-Key": env.HYPIXEL_API_KEY
}
})
if (!res.ok) return null
const { success, data } = guildSchema.safeParse(await res.json())
if (!success) return null
return data.guild
}

View File

@@ -0,0 +1,22 @@
import z from "zod"
const mojangApi = "https://api.mojang.com/users/profiles/minecraft"
const schema = z.object({
name: z.string().min(1),
id: z.string().min(1)
})
export async function getUuid(ign: string) {
const res = await fetch(`${mojangApi}/${ign}`)
if (!res.ok) return null
const data = await res.json()
const parsed = schema.safeParse(data)
if (!parsed.success) return null
return parsed.data.id
}

View File

@@ -0,0 +1,20 @@
import { env } from "../../env/server"
import { playerSchema } from "../../schema/player"
const playerApi = "https://api.hypixel.net/v2/player"
export async function getPlayer(uuid: string) {
const res = await fetch(`${playerApi}?uuid=${uuid}`, {
headers: {
"API-Key": env.HYPIXEL_API_KEY
}
})
if (!res.ok) return null
const { success, data } = playerSchema.safeParse(await res.json())
if (!success) return null
return data.player
}

View File

@@ -0,0 +1,9 @@
export function floorLevel(level: number, base: number) {
const extra = level % base
if (extra === 0) {
return level;
}
return level - extra;
}

31
src/lib/hypixel/guild.ts Normal file
View File

@@ -0,0 +1,31 @@
import { object } from "zod"
import { Guild } from "../schema/guild"
export function getGuildMember(guild: Guild["guild"], uuid: string) {
return guild.members.find(m => m.uuid === uuid)
}
export function getGuildRankTag(guild: Guild["guild"], uuid: string) {
const member = getGuildMember(guild, uuid)
return member?.rank === "Guild Master"
? "[GM]"
: `[${guild.ranks.find(r => r.name === member?.rank)?.tag}]`
}
export function getMemberGEXP(guild: Guild["guild"], uuid: string, days: number = 0) {
const member = getGuildMember(guild, uuid)
if (!member) return null
const index = Object.keys(member.expHistory)[days]
return member.expHistory[index]
}
export function getMemberWeeklyGEXP(guild: Guild["guild"], uuid: string) {
const member = getGuildMember(guild, uuid)
if (!member) return null
return Object.values(member.expHistory).reduce((a, b) => a + b)
}

37
src/lib/hypixel/level.ts Normal file
View File

@@ -0,0 +1,37 @@
const BASE = 10000;
const GROWTH = 2500;
const HALF_GROWTH = 0.5 * GROWTH;
const REVERSE_PQ_PREFIX = -(BASE - 0.5 * GROWTH) / GROWTH;
const REVERSE_CONST = REVERSE_PQ_PREFIX * REVERSE_PQ_PREFIX;
const GROWTH_DIVIDES_2 = 2 / GROWTH;
export function getLevel(exp: number) {
return exp <= 1 ? 1 : Math.floor(1 + REVERSE_PQ_PREFIX + Math.sqrt(REVERSE_CONST + GROWTH_DIVIDES_2 * exp));
}
export function getExactLevel(exp: number) {
return getLevel(exp) + getPercentageToNextLevel(exp);
}
// function getExpFromLevelToNext(level: number) {
// return level < 1 ? BASE : GROWTH * (level - 1) + BASE;
// }
function getTotalExpToLevel(level: number) {
const lv = Math.floor(level); const
x0 = getTotalExpToFullLevel(lv);
if (level === lv) return x0;
return (getTotalExpToFullLevel(lv + 1) - x0) * (level % 1) + x0;
}
function getTotalExpToFullLevel(level: number) {
return (HALF_GROWTH * (level - 2) + BASE) * (level - 1);
}
function getPercentageToNextLevel(exp: number) {
const lv = getLevel(exp);
const x0 = getTotalExpToLevel(lv);
return (exp - x0) / (getTotalExpToLevel(lv + 1) - x0);
}

41
src/lib/hypixel/stats.ts Normal file
View File

@@ -0,0 +1,41 @@
import { MULTIPLIER } from "@/data/hypixel/general"
import { Player } from "@/lib/schema/player"
export function getCoinMultiplier(level: number) {
if (level > MULTIPLIER[MULTIPLIER.length - 1].level) {
return MULTIPLIER[MULTIPLIER.length - 1].value;
}
for (let i = MULTIPLIER.length - 1; i >= 0; i--) {
if (level >= MULTIPLIER[i].level) {
return MULTIPLIER[i].value;
}
}
return MULTIPLIER[0].value
}
export function getTotalCoins(stats: Player["player"]["stats"]) {
return Object.values(stats).reduce((total, stat) => total + (stat.coins || 0), 0);
}
export function getTotalQuests(quests: Player["player"]["quests"]) {
return Object.values(quests).reduce((total, quest) => total + (quest.completions?.length || 0), 0);
}
export function getTotalChallenges(challenges: Player["player"]["challenges"]["all_time"]) {
return Object.values(challenges).reduce((total, challenge) => total + challenge, 0);
}
export function rewardClaimed(claimedAt?: number) {
if (!claimedAt) return false
const now = new Date()
const claimedDate = new Date(claimedAt)
const oneDay = 24 * 60 * 60 * 1000
if (now.getMilliseconds() - claimedDate.getMilliseconds() > oneDay) {
return true
} else {
return false
}
}

View File

@@ -0,0 +1,29 @@
"use server"
import { getUuid } from "./api/mojang"
import { getPlayer } from "./api/player"
export async function validatePlayer(ign: string) {
const uuid = await getUuid(ign)
if (!uuid) {
return {
error: true,
message: "Player not found",
}
}
const player = await getPlayer(uuid)
if (!player) {
return {
error: true,
message: "Player never logged on to Hypixel",
}
}
return {
error: false,
message: "Player found",
}
}

26
src/lib/schema/guild.ts Normal file
View File

@@ -0,0 +1,26 @@
import z from "zod"
export const guildSchema = z.object({
guild: z.object({
_id: z.string().min(1),
name: z.string().min(1),
tag: z.string().optional(),
tagColor: z.string().optional(),
members: z.array(z.object({
uuid: z.string(),
rank: z.string(),
joined: z.number(),
questParticipation: z.number().optional(),
expHistory: z.record(z.string(), z.number())
})),
ranks: z.array(z.object({
name: z.string(),
default: z.boolean(),
tag: z.string().nullish().optional(),
created: z.number(),
priority: z.number()
}))
})
})
export type Guild = z.infer<typeof guildSchema>

56
src/lib/schema/player.ts Normal file
View File

@@ -0,0 +1,56 @@
import z from "zod"
export const playerSchema = z.object({
player: z.object({
displayname: z.string(),
uuid: z.string(),
newPackageRank: z.literal("VIP").or(z.literal("VIP_PLUS").or(z.literal("MVP")).or(z.literal("MVP_PLUS"))).optional(),
monthlyPackageRank: z.string().optional(),
rankPlusColor: z.string().optional(),
monthlyRankColor: z.literal("GOLD").or(z.literal("AQUA")).optional(),
networkExp: z.number(),
karma: z.number(),
achievementPoints: z.number().optional(),
stats: z.record(
z.string(),
z.object({
coins: z.number().optional()
})
),
quests: z.record(
z.string(),
z.object({
completions: z.array(
z.object({
time: z.number()
}).optional()
).optional()
})
),
challenges: z.object({
all_time: z.record(z.string(), z.number())
}),
lastClaimedReward: z.number().optional(),
rewardHighScore: z.number().optional(),
rewardStreak: z.number().optional(),
totalRewards: z.number().optional(),
giftingMeta: z.object({
giftsGiven: z.number().optional(),
ranksGiven: z.number().optional()
}).optional(),
firstLogin: z.number().optional(),
lastLogin: z.number().optional(),
socialMedia: z.object({
links: z.object({
DISCORD: z.string().optional(),
TWITCH: z.string().optional(),
HYPIXEL: z.string().optional(),
TWITTER: z.string().optional(),
YOUTUBE: z.string().optional()
})
})
})
})
export type Player = z.infer<typeof playerSchema>