From 4417485c9373423194462dcc820b23a4d3143d11 Mon Sep 17 00:00:00 2001 From: Taken Date: Tue, 16 Sep 2025 13:11:59 +0200 Subject: [PATCH] Added drag and drop --- bun.lock | 17 +++ package.json | 5 + src/app/(stats)/player/[ign]/_client.tsx | 157 ++++++++++++++++++++--- src/app/(stats)/player/[ign]/page.tsx | 8 +- 4 files changed, 167 insertions(+), 20 deletions(-) diff --git a/bun.lock b/bun.lock index 62499ee..9212ddd 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "": { "name": "stats-hypixel", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-separator": "^1.1.7", @@ -13,6 +16,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "geist": "^1.4.2", + "js-cookie": "^3.0.5", "lucide-react": "^0.528.0", "motion": "^12.23.12", "next": "15.5.2", @@ -30,6 +34,7 @@ "@eslint/eslintrc": "^3", "@next/eslint-plugin-next": "15.5.2", "@tailwindcss/postcss": "^4", + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/react": "19.1.12", "@types/react-dom": "19.1.9", @@ -62,6 +67,14 @@ "@designbycode/tailwindcss-text-stroke": ["@designbycode/tailwindcss-text-stroke@1.3.0", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || >=3.0.0-alpha.1" } }, "sha512-EyZi2EDv+/v55JF7OFrPUUJHr0r/C9bZtvhWNpamMj5MjAEMqBMhcO1ZW9aXAD2viszgtlnYLIta80NJtsuy6w=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], + + "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], + + "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], + + "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@dprint/darwin-arm64": ["@dprint/darwin-arm64@0.50.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NNKf3dxXn567pd/hpCVLHLbC0dI7s3YvQnUEwjRTOAQVMp6O7/ME+Tg1RPGsDP1IB+Y2fIYSM4qmG02zQrqjAQ=="], "@dprint/darwin-x64": ["@dprint/darwin-x64@0.50.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-PcY75U3UC/0CLOxWzE0zZJZ2PxzUM5AX2baYL1ovgDGCfqO1H0hINiyxfx/8ncGgPojWBkLs+zrcFiGnXx7BQg=="], @@ -278,6 +291,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], @@ -644,6 +659,8 @@ "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], diff --git a/package.json b/package.json index d56f30d..0c2455b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "fmt": "dprint fmt src/**/*.ts src/**/*.tsx" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-separator": "^1.1.7", @@ -22,6 +25,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "geist": "^1.4.2", + "js-cookie": "^3.0.5", "lucide-react": "^0.528.0", "motion": "^12.23.12", "next": "15.5.2", @@ -39,6 +43,7 @@ "@eslint/eslintrc": "^3", "@next/eslint-plugin-next": "15.5.2", "@tailwindcss/postcss": "^4", + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/react": "19.1.12", "@types/react-dom": "19.1.9", diff --git a/src/app/(stats)/player/[ign]/_client.tsx b/src/app/(stats)/player/[ign]/_client.tsx index 9e13247..5d23a0e 100644 --- a/src/app/(stats)/player/[ign]/_client.tsx +++ b/src/app/(stats)/player/[ign]/_client.tsx @@ -1,7 +1,14 @@ "use client" import { Accordion } from "@/components/ui/accordion" +import { closestCenter, DndContext, DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core" +import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable" +import { useSortable } from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" +import Cookies from "js-cookie" +import { GripVertical } from "lucide-react" import { usePathname } from "next/navigation" +import { useEffect, useState } from "react" import { Player } from "@/lib/schema/player" import ArcadeStats from "./_stats/arcade/arcade" @@ -18,6 +25,41 @@ import TNTGamesStats from "./_stats/tnt-games/tnt-games" import UHCStats from "./_stats/uhc/uhc" import WoolGamesStats from "./_stats/woolgames/woolgames" +interface SortableStatItemProps { + id: string + children: React.ReactNode +} + +function SortableStatItem({ id, children }: SortableStatItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1 + } + + return ( +
+
+ +
+ {children} +
+ ) +} + export function PlayerPageLoadText() { const path = usePathname() @@ -25,28 +67,105 @@ export function PlayerPageLoadText() { } export default function PlayerStats( - { stats, achievements }: { stats: NonNullable, achievements: Player["player"]["achievements"] } + { stats, achievements, layout }: { + stats: NonNullable + achievements: Player["player"]["achievements"] + layout: string[] | undefined + } ) { + const statsComponents = { + "bedwars": , + "skywars": ( + + ), + "duels": , + "murder-mystery": , + "build-battle": , + "uhc": , + "pit": , + "tnt-games": , + "megawalls": , + "copsandcrims": , + "woolgames": , + "blitz": , + "arcade": + } as const + + const defaultOrder = Object.keys(statsComponents).sort() + const orderToUse = layout || defaultOrder + + const [statsOrder, setStatsOrder] = useState(layout || defaultOrder) + const [isClient, setIsClient] = useState(false) + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates + }) + ) + + function updateStatsOrder(arr: string[]) { + Cookies.set("player-stats-order", JSON.stringify(arr), { + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + expires: 365 + }) + } + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event + + if (over && active.id !== over.id) { + const oldIndex = statsOrder.indexOf(active.id as string) + const newIndex = statsOrder.indexOf(over.id as string) + const newOrder = arrayMove(statsOrder, oldIndex, newIndex) + + setStatsOrder(newOrder) + updateStatsOrder(newOrder) + } + } + + useEffect(() => { + setIsClient(true) + if (layout && layout.length > 0) { + setStatsOrder(layout) + } + }, [layout]) + + if (!isClient) { + return ( +
+ + {orderToUse.map((statKey) => ( +
+ {statsComponents[statKey as keyof typeof statsComponents]} +
+ ))} +
+
+ ) + } + return (
- - - - - - - - - - - - - - - + + + + {statsOrder.map((statKey) => ( + + {statsComponents[statKey as keyof typeof statsComponents]} + + ))} + + +
) } diff --git a/src/app/(stats)/player/[ign]/page.tsx b/src/app/(stats)/player/[ign]/page.tsx index c192519..64e96a4 100644 --- a/src/app/(stats)/player/[ign]/page.tsx +++ b/src/app/(stats)/player/[ign]/page.tsx @@ -8,7 +8,9 @@ import { getPlayer } from "@/lib/hypixel/api/player" import { getExactLevel } from "@/lib/hypixel/general/level" import { Loader2Icon, ShieldAlert } from "lucide-react" import { Metadata } from "next" +import { cookies } from "next/headers" import { Suspense } from "react" +import z from "zod" import PlayerStats, { PlayerPageLoadText } from "./_client" import Sidebar from "./_components/Sidebar" @@ -46,6 +48,7 @@ export default function PlayerPage({ params }: PageProps<"/player/[ign]">) { async function SuspendedPage({ params }: Pick, "params">) { const { ign: pign } = await params + const c = await cookies() const mc = await getUuid(pign) if (!mc) { @@ -70,6 +73,9 @@ async function SuspendedPage({ params }: Pick, "param const guild = await getGuild(mc.id) const level = getExactLevel(player.networkExp) + const schema = z.array(z.string()) + const { data: layout } = schema.safeParse(JSON.parse(c.get("stats-order")?.value ?? "null")) + return (
, "param session={session} /> {player.stats !== undefined ? - : + : (