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

View File

@@ -0,0 +1,105 @@
import { getColor } from "@/data/colors"
import { Player } from "@/lib/schema/player"
type NewPackageRank = Player["player"]["newPackageRank"]
type MonthlyPackageRank = Player["player"]["monthlyPackageRank"]
type RankColor = Player["player"]["monthlyRankColor"]
export default function DisplayName(
{ ign, rank, monthly, rankColor, plusColor, guildTag, tagColor }: {
ign: string
rank: NewPackageRank
monthly: MonthlyPackageRank
rankColor: RankColor
plusColor: string | undefined
guildTag: string | undefined
tagColor: string | undefined
}
) {
return (
<>
<PlayerRank rank={rank} monthly={monthly} plusColor={plusColor} rankColor={rankColor} />{" "}
<PlayerIGN ign={ign} rank={rank} monthly={monthly} rankColor={rankColor} /> <GuildTag tag={guildTag} tagColor={tagColor} />
</>
)
}
function PlayerIGN({ ign, rank, monthly, rankColor }: { ign: string, rank: NewPackageRank, monthly: MonthlyPackageRank, rankColor: RankColor }) {
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 }: { rank: NewPackageRank, monthly: MonthlyPackageRank, plusColor?: string, rankColor: RankColor }
) {
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
}
}
function GuildTag({ tag, tagColor }: { tag?: string, tagColor?: string }) {
if (!tag) return null
const color = getColor(tagColor, "text", "gray")
return <span className={color}>[{tag}]</span>
}

View File

@@ -0,0 +1,57 @@
"use client"
import { Input } from "@/components/ui/input"
import { validatePlayer } from "@/lib/hypixel/validatePlayer"
import { cn } from "@/lib/utils"
import { Search } from "lucide-react"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { toast } from "sonner"
export function SearchBar({ navbar }: { navbar?: boolean }) {
const [input, setInput] = useState("")
const router = useRouter()
async function handleSearch(e: React.FormEvent) {
e.preventDefault()
const validatedPlayer = await validatePlayer(input.trim())
if (validatedPlayer.error === true) {
toast.error(validatedPlayer.message)
setInput("")
return
}
if (input.trim()) {
router.push(`/player/${encodeURIComponent(input.trim())}`)
}
if (navbar) {
setInput("")
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch(e)
}
}
return (
<div className={cn("w-full max-w-4xl px-4", !navbar && "mt-8")}>
<form onSubmit={handleSearch}>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
type="text"
placeholder={!navbar ? "Search for a player..." : ""}
className="pl-10"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
</form>
</div>
)
}

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@@ -0,0 +1,25 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }