"use client"; import type { ReactNode } from "react"; import { Children, isValidElement, useEffect, useRef, useState } from "react"; import { ActionIcon, Badge, Collapse, Group, Stack, Tabs, Text, UnstyledButton } from "@mantine/core"; import { IconChevronDown, IconChevronLeft, IconChevronRight, IconChevronUp } from "@tabler/icons-react"; interface PaginatedListProps { children: ReactNode; pageSize?: number; } export function PaginatedList({ children, pageSize = 6 }: PaginatedListProps) { const items = Children.toArray(children); const totalPages = Math.max(1, Math.ceil(items.length / pageSize)); const [page, setPage] = useState(0); const prevCountRef = useRef(items.length); useEffect(() => { if (items.length !== prevCountRef.current) { setPage(0); prevCountRef.current = items.length; } }, [items.length]); const clampedPage = Math.min(page, totalPages - 1); const start = clampedPage * pageSize; const visible = items.slice(start, start + pageSize); return (
{visible}
{totalPages > 1 && ( setPage((p) => Math.max(0, p - 1))} > {clampedPage + 1} / {totalPages} = totalPages - 1} onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))} > )}
); } interface TabsContainerProps { children: ReactNode; defaultTab?: string; } function isTabPanel(child: ReactNode): child is React.ReactElement { return isValidElement(child) && child.type === TabPanel && typeof child.props.value === "string"; } export function TabsContainer({ children, defaultTab }: TabsContainerProps) { const panels = Children.toArray(children).filter(isTabPanel); const firstPanel = panels[0]; return ( {panels.map((panel) => ( {panel.props.label ?? panel.props.value} ))} {panels.map((panel) => ( {panel.props.children} ))} ); } interface TabPanelProps { children: ReactNode; value: string; label?: string; } export function TabPanel({ children }: TabPanelProps) { return <>{children}; } interface CollapsibleProps { children: ReactNode; title: string; defaultOpen?: boolean; } const COLLAPSE_CHEVRONS = { closed: IconChevronDown, open: IconChevronUp, } as const; const COLLAPSE_STATE: Record<0 | 1, keyof typeof COLLAPSE_CHEVRONS> = { 0: "closed", 1: "open", }; export function Collapsible({ children, title, defaultOpen = false }: CollapsibleProps) { const [opened, setOpened] = useState(defaultOpen); const Chevron = COLLAPSE_CHEVRONS[COLLAPSE_STATE[Number(opened) as 0 | 1]]; return ( setOpened((o) => !o)} py={4}> {title} {children} ); } interface StatBarProps { value: number; max?: number; label?: string; color?: string; } function calcPct(value: number, max: number): number { if (max <= 0) return 0; return Math.min((value / max) * 100, 100); } export function StatBar({ value, max = 100, label, color = "blue" }: StatBarProps) { const pct = calcPct(value, max); return ( {label && ( {label} )}
{value} ); } interface TypeBadgeProps { type: string; size?: "xs" | "sm" | "md" | "lg" | "xl"; } const TYPE_COLORS: Record = { normal: "gray", fire: "red", water: "blue", electric: "yellow", grass: "green", ice: "cyan", fighting: "orange", poison: "grape", ground: "yellow", flying: "indigo", psychic: "pink", bug: "lime", rock: "orange", ghost: "violet", dragon: "indigo", dark: "dark", steel: "gray", fairy: "pink", }; function normalizeType(type: unknown): string { if (typeof type === "string") return type; if (type == null) return ""; return String(type); } export function TypeBadge({ type, size = "sm" }: TypeBadgeProps) { const typeStr = normalizeType(type); const color = TYPE_COLORS[typeStr.toLowerCase()] ?? "gray"; return ( {typeStr} ); }