Finished sidebar
This commit is contained in:
22
src/app/(main)/layout.tsx
Normal file
22
src/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Settings } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<>
|
||||
<nav className="flex items-center justify-between px-6 py-4 border-b">
|
||||
<Link href="/">
|
||||
<span className="font-semibold text-lg">Hypixel Stats</span>
|
||||
</Link>
|
||||
<Button variant="ghost" size="icon" aria-label="Settings">
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
</nav>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
12
src/app/(main)/page.tsx
Normal file
12
src/app/(main)/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SearchBar } from "@/components/search-bar"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center">
|
||||
<div className="mt-[20vh]">
|
||||
<h1 className="text-4xl font-bold text-center">Stats Hypixel</h1>
|
||||
</div>
|
||||
<SearchBar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
24
src/app/(stats)/layout.tsx
Normal file
24
src/app/(stats)/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { SearchBar } from "@/components/search-bar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Settings } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<>
|
||||
<nav className="flex items-center justify-between px-6 py-4 border-b">
|
||||
<Link href="/">
|
||||
<span className="font-semibold text-lg">Hypixel Stats</span>
|
||||
</Link>
|
||||
<SearchBar navbar />
|
||||
<Button variant="ghost" size="icon" aria-label="Settings">
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
</nav>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
167
src/app/(stats)/player/[ign]/_components/Sidebar.tsx
Normal file
167
src/app/(stats)/player/[ign]/_components/Sidebar.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { getColor } from "@/data/colors"
|
||||
import { formatDate, formatNumber } from "@/lib/formatters"
|
||||
import { getGuildMember, getGuildRankTag, getMemberGEXP, getMemberWeeklyGEXP } from "@/lib/hypixel/guild"
|
||||
import { getCoinMultiplier, getTotalChallenges, getTotalCoins, getTotalQuests, rewardClaimed } from "@/lib/hypixel/stats"
|
||||
import { Guild } from "@/lib/schema/guild"
|
||||
import { Player } from "@/lib/schema/player"
|
||||
import { Separator } from "@radix-ui/react-separator"
|
||||
import Link from "next/link"
|
||||
import SocialIcons from "./SocialIcons"
|
||||
|
||||
type SidebarProps = {
|
||||
level: number
|
||||
ign: string
|
||||
player: Player["player"]
|
||||
guild: Guild["guild"] | undefined
|
||||
}
|
||||
|
||||
export default function Sidebar({ level, ign, player, guild }: SidebarProps) {
|
||||
return (
|
||||
<Card className="w-1/4">
|
||||
<CardContent>
|
||||
<div className="flex justify-between px-8">
|
||||
<div className="text-center">
|
||||
<p>Hypixel level</p>
|
||||
<p>{level.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p>Karma</p>
|
||||
<p className="text-mc-light-purple">{formatNumber(player.karma)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div>
|
||||
<p>
|
||||
<span className="font-bold">{"Coin multiplier: "}</span>
|
||||
<span>{`x${getCoinMultiplier(level)} (Level ${level.toFixed(1).split(".")[0]})`}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Total coins: "}</span>
|
||||
<span className="text-mc-gold">{formatNumber(getTotalCoins(player.stats))}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
<Link href={`/achievements/${ign}`} className="font-bold underline">
|
||||
Achievement Points
|
||||
</Link>
|
||||
</span>
|
||||
<span className="font-bold">{": "}</span>
|
||||
<span>{formatNumber(player.achievementPoints ?? 0)}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
<Link href={`/quests/${ign}`} className="font-bold underline">
|
||||
Quests Completed
|
||||
</Link>
|
||||
</span>
|
||||
<span className="font-bold">{": "}</span>
|
||||
<span>{formatNumber(getTotalQuests(player.quests))}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Challenges Completed: "}</span>
|
||||
<span>{formatNumber(getTotalChallenges(player.challenges.all_time))}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div>
|
||||
<p>
|
||||
<span className="font-bold">{"Today's Reward: "}</span>
|
||||
<span>{rewardClaimed(player.lastClaimedReward) ? "Claimed" : "Unclaimed"}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Rewards Claimed: "}</span>
|
||||
<span>{player.totalRewards}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Reward Streak: "}</span>
|
||||
<span>{player.rewardStreak}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Top Reward Streak: "}</span>
|
||||
<span>{player.rewardHighScore}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div>
|
||||
<p>
|
||||
<span className="font-bold">{"Gifts Given: "}</span>
|
||||
<span>{player.giftingMeta?.giftsGiven ?? 0}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Ranks Given: "}</span>
|
||||
<span>{player.giftingMeta?.ranksGiven ?? 0}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div>
|
||||
<p>
|
||||
<span className="font-bold">{"First Login: "}</span>
|
||||
<span>{formatDate(player.firstLogin ?? 0)}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Last Login: "}</span>
|
||||
<span>{formatDate(player.lastLogin ?? 0)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
{guild && (
|
||||
<>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<Link href={`/guild/${ign}`}>
|
||||
<h1 className="text-xl font-bold underline">Guild</h1>
|
||||
</Link>
|
||||
<p>
|
||||
<span className="font-bold">{"Name: "}</span>
|
||||
<span className={getColor(guild.tagColor, "text", "gray")}>{guild.name}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Members: "}</span>
|
||||
<span>{guild.members.length}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<span className="font-bold">{"Rank: "}</span>
|
||||
<span>{`${getGuildMember(guild, player.uuid)?.rank} `}</span>
|
||||
<span className={getColor(guild.tagColor, "text", "gray")}>
|
||||
{getGuildRankTag(guild, player.uuid)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Daily GEXP: "}</span>
|
||||
<span>{formatNumber(getMemberGEXP(guild, player.uuid, 0) ?? 0)}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Weekly GEXP: "}</span>
|
||||
<span>{formatNumber(getMemberWeeklyGEXP(guild, player.uuid) ?? 0)}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-bold">{"Joined: "}</span>
|
||||
<span>{formatDate(getGuildMember(guild, player.uuid)?.joined ?? 0)}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-xl font-bold underline">Social Links</h1>
|
||||
<div className="flex gap-2">
|
||||
<SocialIcons
|
||||
discord={player.socialMedia.links.DISCORD}
|
||||
twitch={player.socialMedia.links.TWITCH}
|
||||
youtube={player.socialMedia.links.YOUTUBE}
|
||||
twitter={player.socialMedia.links.TWITCH}
|
||||
hypixel={player.socialMedia.links.HYPIXEL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
74
src/app/(stats)/player/[ign]/_components/SocialIcons.tsx
Normal file
74
src/app/(stats)/player/[ign]/_components/SocialIcons.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CopyIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { CgWebsite } from "react-icons/cg"
|
||||
import { FaDiscord, FaTwitch, FaYoutube } from "react-icons/fa"
|
||||
import { FiTwitter } from "react-icons/fi"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function SocialIcons(
|
||||
{ discord, twitch, youtube, twitter, hypixel }: { discord?: string, twitch?: string, youtube?: string, twitter?: string, hypixel?: string }
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<DiscordIcon username={discord} />
|
||||
<SocialIcon href={twitch}>
|
||||
<FaTwitch />
|
||||
</SocialIcon>
|
||||
<SocialIcon href={youtube}>
|
||||
<FaYoutube />
|
||||
</SocialIcon>
|
||||
<SocialIcon href={twitter}>
|
||||
<FiTwitter />
|
||||
</SocialIcon>
|
||||
<SocialIcon href={hypixel}>
|
||||
<CgWebsite />
|
||||
</SocialIcon>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DiscordIcon({ username }: { username?: string }) {
|
||||
if (!username) return null
|
||||
|
||||
function handleClick() {
|
||||
toast(
|
||||
<div className="flex gap-8">
|
||||
<h1 className="text-2xl">{username}</h1>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(username!)
|
||||
toast.dismiss("discord-username")
|
||||
}}
|
||||
>
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</div>,
|
||||
{
|
||||
position: "bottom-center",
|
||||
id: "discord-username",
|
||||
className: "flex justify-center items-center gap-4"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="ghost" className="transition-all hover:scale-125 hover:cursor-pointer" onClick={handleClick}>
|
||||
<FaDiscord />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SocialIcon({ href, children }: { href?: string, children: React.ReactNode }) {
|
||||
if (!href) return null
|
||||
|
||||
return (
|
||||
<Button variant="ghost" className="transition-all hover:scale-125" asChild>
|
||||
<Link href={href}>
|
||||
{children}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
63
src/app/(stats)/player/[ign]/page.tsx
Normal file
63
src/app/(stats)/player/[ign]/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import DisplayName from "@/components/player/displayname"
|
||||
import { getGuild } from "@/lib/hypixel/api/guild"
|
||||
import { getUuid } from "@/lib/hypixel/api/mojang"
|
||||
import { getPlayer } from "@/lib/hypixel/api/player"
|
||||
import { getExactLevel } from "@/lib/hypixel/level"
|
||||
import Sidebar from "./_components/Sidebar"
|
||||
|
||||
export default async function PlayerPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ ign: string }>
|
||||
}) {
|
||||
const { ign: pign } = await params
|
||||
|
||||
const uuid = await getUuid(pign)
|
||||
if (!uuid) {
|
||||
return (
|
||||
<div className="flex flex-col items-center min-h-screen">
|
||||
<h1 className="mt-25">Player not found</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const player = await getPlayer(uuid)
|
||||
|
||||
if (!player) {
|
||||
return (
|
||||
<div className="flex flex-col items-center min-h-screen">
|
||||
<h1 className="mt-25">Player not found</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const guild = await getGuild(uuid)
|
||||
|
||||
const level = getExactLevel(player.networkExp)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center min-h-screen">
|
||||
<h1 className="text-3xl font-bold mt-25">
|
||||
<DisplayName
|
||||
ign={player.displayname}
|
||||
rank={player.newPackageRank}
|
||||
monthly={player.monthlyPackageRank}
|
||||
rankColor={player.monthlyRankColor}
|
||||
plusColor={player.rankPlusColor}
|
||||
guildTag={guild?.tag}
|
||||
tagColor={guild?.tagColor}
|
||||
/>
|
||||
</h1>
|
||||
<h1>
|
||||
{player.uuid}
|
||||
</h1>
|
||||
<div className="flex gap-6 px-6 mt-8 w-full max-w-7xl">
|
||||
<Sidebar level={level} ign={pign} player={player} guild={guild ?? undefined} />
|
||||
<div className="p-6 w-3/4 rounded-xl border shadow-sm bg-card">
|
||||
<h2 className="mb-4 text-xl font-semibold">Game Statistics</h2>
|
||||
<p>Game stats will be displayed here...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,26 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-mc-black: #000000;
|
||||
--color-mc-dark-blue: #0000AA;
|
||||
--color-mc-dark-green: #00AA00;
|
||||
--color-mc-dark-aqua: #00AAAA;
|
||||
--color-mc-dark-red: #AA0000;
|
||||
--color-mc-dark-purple: #AA00AA;
|
||||
--color-mc-gold: #FFAA00;
|
||||
--color-mc-gray: #AAAAAA;
|
||||
--color-mc-light-gray: #C6C6C6;
|
||||
--color-mc-dark-gray: #555555;
|
||||
--color-mc-blue: #5555FF;
|
||||
--color-mc-green: #55FF55;
|
||||
--color-mc-aqua: #55FFFF;
|
||||
--color-mc-red: #FF5555;
|
||||
--color-mc-light-purple: #FF55FF;
|
||||
--color-mc-yellow: #FFFF55;
|
||||
--color-mc-white: #FFFFFF;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
@@ -114,6 +134,7 @@
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ReactNode } from "react"
|
||||
import "./globals.css"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="antialiased">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="antialiased dark">
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
Hello
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user