veza/apps/web/desy/components/ui/gaming.tsx
2026-01-22 17:23:11 +01:00

358 lines
13 KiB
TypeScript

"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
/* ═══════════════════════════════════════════════════════════════════════════
STAT BAR — HP / MP / XP Style
═══════════════════════════════════════════════════════════════════════════ */
const statBarVariants = cva(
"relative h-6 rounded-sm border-2 overflow-hidden bg-[var(--void-900)]",
{
variants: {
variant: {
hp: "border-[var(--hp-red)]",
mp: "border-[var(--mp-blue)]",
xp: "border-[var(--xp-gold)]",
shield: "border-[var(--shield-purple)]",
},
size: {
sm: "h-4 text-[10px]",
default: "h-6 text-xs",
lg: "h-8 text-sm",
},
},
defaultVariants: {
variant: "xp",
size: "default",
},
}
)
const statBarFillVariants = cva(
"h-full transition-all duration-500 ease-out relative",
{
variants: {
variant: {
hp: "bg-gradient-to-b from-[var(--hp-red)] to-[#990000] shadow-[inset_0_-2px_4px_rgba(0,0,0,0.3),0_0_10px_rgba(255,51,51,0.5)]",
mp: "bg-gradient-to-b from-[var(--mp-blue)] to-[#0066cc] shadow-[inset_0_-2px_4px_rgba(0,0,0,0.3),0_0_10px_rgba(51,153,255,0.5)]",
xp: "bg-gradient-to-b from-[var(--xp-gold)] to-[#cc9900] shadow-[inset_0_-2px_4px_rgba(0,0,0,0.3),0_0_10px_rgba(255,215,0,0.5)]",
shield: "bg-gradient-to-b from-[var(--shield-purple)] to-[#6a1b9a] shadow-[inset_0_-2px_4px_rgba(0,0,0,0.3),0_0_10px_rgba(156,39,176,0.5)]",
},
},
defaultVariants: {
variant: "xp",
},
}
)
interface StatBarProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof statBarVariants> {
value: number
max?: number
showLabel?: boolean
label?: string
animated?: boolean
}
const StatBar = React.forwardRef<HTMLDivElement, StatBarProps>(
({ className, variant, size, value, max = 100, showLabel = true, label, animated = false, ...props }, ref) => {
const percentage = Math.min(100, Math.max(0, (value / max) * 100))
return (
<div
ref={ref}
className={cn(statBarVariants({ variant, size }), className)}
{...props}
>
<div
className={cn(statBarFillVariants({ variant }), animated && "animate-[bar-fill_1s_ease-out]")}
style={{ width: `${percentage}%` }}
/>
{showLabel && (
<div className="absolute inset-0 flex items-center justify-center font-display font-bold text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]">
{label || `${value}/${max}`}
</div>
)}
</div>
)
}
)
StatBar.displayName = "StatBar"
/* ═══════════════════════════════════════════════════════════════════════════
LEVEL BADGE
═══════════════════════════════════════════════════════════════════════════ */
interface LevelBadgeProps extends React.HTMLAttributes<HTMLDivElement> {
level: number
size?: "sm" | "default" | "lg"
animated?: boolean
}
const LevelBadge = React.forwardRef<HTMLDivElement, LevelBadgeProps>(
({ className, level, size = "default", animated = false, ...props }, ref) => {
const sizes = {
sm: "w-10 h-10 text-sm",
default: "w-12 h-12 text-xl",
lg: "w-16 h-16 text-2xl",
}
return (
<div
ref={ref}
className={cn(
"relative inline-flex items-center justify-center rounded-full",
"bg-gradient-to-b from-[#3a3a3a] to-[#1a1a1a]",
"border-[3px] border-[var(--xp-gold)]",
"font-display font-black text-[var(--xp-gold)]",
"shadow-[0_0_20px_rgba(255,215,0,0.3),inset_0_2px_4px_rgba(255,255,255,0.1)]",
animated && "animate-[level-up_0.5s_ease-out]",
sizes[size],
className
)}
style={{ textShadow: "0 0 10px var(--xp-gold)" }}
{...props}
>
{level}
</div>
)
}
)
LevelBadge.displayName = "LevelBadge"
/* ═══════════════════════════════════════════════════════════════════════════
ACHIEVEMENT POPUP
═══════════════════════════════════════════════════════════════════════════ */
interface AchievementProps extends React.HTMLAttributes<HTMLDivElement> {
icon: React.ReactNode
title: string
name: string
xp?: number
animated?: boolean
}
const Achievement = React.forwardRef<HTMLDivElement, AchievementProps>(
({ className, icon, title, name, xp, animated = true, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"flex items-center gap-4 p-4",
"bg-gradient-to-b from-[rgba(30,30,40,0.95)] to-[rgba(20,20,30,0.95)]",
"border-2 border-[var(--xp-gold)] rounded-lg",
"shadow-[0_0_30px_rgba(255,215,0,0.3)]",
animated && "animate-achievement",
className
)}
{...props}
>
<div className="w-14 h-14 bg-gradient-to-br from-[var(--xp-gold)] to-[#cc9900] rounded-lg flex items-center justify-center text-2xl">
{icon}
</div>
<div className="flex-1">
<p className="font-display text-xs uppercase tracking-widest text-[var(--xp-gold)]">
{title}
</p>
<p className="font-sans text-lg font-bold text-white">{name}</p>
{xp && (
<p className="text-xs text-[var(--xp-gold)]">+{xp} XP</p>
)}
</div>
</div>
)
}
)
Achievement.displayName = "Achievement"
/* ═══════════════════════════════════════════════════════════════════════════
SKILL TREE NODE
═══════════════════════════════════════════════════════════════════════════ */
interface SkillNodeProps extends React.HTMLAttributes<HTMLDivElement> {
icon: React.ReactNode
name: string
unlocked?: boolean
active?: boolean
level?: number
maxLevel?: number
}
const SkillNode = React.forwardRef<HTMLDivElement, SkillNodeProps>(
({ className, icon, name, unlocked = false, active = false, level = 0, maxLevel = 5, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"flex flex-col items-center gap-2 p-3 rounded-lg transition-all duration-300",
unlocked ? "opacity-100" : "opacity-40",
active && "scale-105",
className
)}
{...props}
>
<div
className={cn(
"w-16 h-16 rounded-lg flex items-center justify-center text-2xl",
"border-2 transition-all duration-300",
unlocked
? "bg-gradient-to-b from-[var(--void-700)] to-[var(--void-900)] border-[var(--cyan-500)] glow-cyan"
: "bg-[var(--void-900)] border-[var(--void-600)]"
)}
>
{icon}
</div>
<p className="text-xs font-medium text-center">{name}</p>
{maxLevel > 1 && (
<div className="flex gap-1">
{Array.from({ length: maxLevel }).map((_, i) => (
<div
key={i}
className={cn(
"w-2 h-2 rounded-full",
i < level ? "bg-[var(--cyan-500)]" : "bg-[var(--void-600)]"
)}
/>
))}
</div>
)}
</div>
)
}
)
SkillNode.displayName = "SkillNode"
/* ═══════════════════════════════════════════════════════════════════════════
STATS PANEL
═══════════════════════════════════════════════════════════════════════════ */
interface StatsPanelProps extends React.HTMLAttributes<HTMLDivElement> {
stats: Array<{
label: string
value: number | string
icon?: React.ReactNode
change?: number
}>
}
const StatsPanel = React.forwardRef<HTMLDivElement, StatsPanelProps>(
({ className, stats, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"p-4 rounded-lg",
"bg-gradient-to-b from-[rgba(30,30,40,0.95)] to-[rgba(20,20,30,0.95)]",
"border-2 border-[var(--xp-gold)] rounded-sm",
"shadow-[inset_0_1px_0_rgba(255,255,255,0.05),0_4px_20px_rgba(0,0,0,0.5)]",
className
)}
{...props}
>
{/* Gaming-style header bar */}
<div className="h-1 -mt-4 -mx-4 mb-4 bg-gradient-to-r from-[var(--hp-red)] via-[var(--mp-blue)] to-[var(--xp-gold)]" />
<div className="grid gap-3">
{stats.map((stat, i) => (
<div key={i} className="flex items-center justify-between">
<div className="flex items-center gap-2">
{stat.icon && <span className="text-[var(--xp-gold)]">{stat.icon}</span>}
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{stat.label}
</span>
</div>
<div className="flex items-center gap-2">
<span className="font-display font-bold text-white">
{stat.value}
</span>
{stat.change !== undefined && (
<span className={cn(
"text-xs",
stat.change > 0 ? "text-[var(--success)]" : stat.change < 0 ? "text-[var(--hp-red)]" : "text-muted-foreground"
)}>
{stat.change > 0 ? "+" : ""}{stat.change}
</span>
)}
</div>
</div>
))}
</div>
</div>
)
}
)
StatsPanel.displayName = "StatsPanel"
/* ═══════════════════════════════════════════════════════════════════════════
INVENTORY SLOT
═══════════════════════════════════════════════════════════════════════════ */
interface InventorySlotProps extends React.HTMLAttributes<HTMLDivElement> {
item?: {
icon: React.ReactNode
rarity?: "common" | "uncommon" | "rare" | "epic" | "legendary"
quantity?: number
}
size?: "sm" | "default" | "lg"
selected?: boolean
}
const rarityColors = {
common: "border-[var(--void-400)]",
uncommon: "border-[var(--success)]",
rare: "border-[var(--mp-blue)]",
epic: "border-[var(--shield-purple)]",
legendary: "border-[var(--xp-gold)] glow-gold",
}
const InventorySlot = React.forwardRef<HTMLDivElement, InventorySlotProps>(
({ className, item, size = "default", selected = false, ...props }, ref) => {
const sizes = {
sm: "w-10 h-10",
default: "w-14 h-14",
lg: "w-20 h-20",
}
return (
<div
ref={ref}
className={cn(
"relative rounded-lg border-2 bg-[var(--void-900)] transition-all duration-200",
"hover:bg-[var(--void-800)] cursor-pointer",
sizes[size],
item ? rarityColors[item.rarity || "common"] : "border-[var(--void-700)]",
selected && "ring-2 ring-[var(--cyan-500)] ring-offset-2 ring-offset-background",
className
)}
{...props}
>
{item && (
<>
<div className="absolute inset-0 flex items-center justify-center text-xl">
{item.icon}
</div>
{item.quantity && item.quantity > 1 && (
<div className="absolute bottom-0 right-0 px-1 text-[10px] font-bold bg-black/70 rounded-tl">
{item.quantity}
</div>
)}
</>
)}
</div>
)
}
)
InventorySlot.displayName = "InventorySlot"
export {
StatBar,
LevelBadge,
Achievement,
SkillNode,
StatsPanel,
InventorySlot,
}