Added drag and drop

This commit is contained in:
2025-09-16 13:11:59 +02:00
parent ff7df6a309
commit 4417485c93
4 changed files with 167 additions and 20 deletions

View File

@@ -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 (
<div ref={setNodeRef} style={style} className="relative group">
<div
{...attributes}
{...listeners}
className="absolute -left-6 top-1/2 z-10 p-1 opacity-0 transition-opacity -translate-y-1/2 group-hover:opacity-50 hover:opacity-100 cursor-grab active:cursor-grabbing"
>
<GripVertical className="size-4 text-muted-foreground" />
</div>
{children}
</div>
)
}
export function PlayerPageLoadText() {
const path = usePathname()
@@ -25,28 +67,105 @@ export function PlayerPageLoadText() {
}
export default function PlayerStats(
{ stats, achievements }: { stats: NonNullable<Player["player"]["stats"]>, achievements: Player["player"]["achievements"] }
{ stats, achievements, layout }: {
stats: NonNullable<Player["player"]["stats"]>
achievements: Player["player"]["achievements"]
layout: string[] | undefined
}
) {
const statsComponents = {
"bedwars": <BedwarsStats stats={stats.Bedwars} />,
"skywars": (
<SkyWarsStats
stats={stats.SkyWars}
achievements_skywars_opal_obsession={achievements?.["skywars_opal_obsession"] ?? 0}
/>
),
"duels": <DuelsStats stats={stats.Duels} />,
"murder-mystery": <MurderMysteryStats stats={stats.MurderMystery} />,
"build-battle": <BuildBattleStats stats={stats.BuildBattle} />,
"uhc": <UHCStats stats={stats.UHC} />,
"pit": <PitStats stats={stats.Pit} />,
"tnt-games": <TNTGamesStats stats={stats.TNTGames} />,
"megawalls": <MegaWallsStats stats={stats.MegaWalls} />,
"copsandcrims": <CopsAndCrimsStats stats={stats.CopsAndCrims} />,
"woolgames": <WoolGamesStats stats={stats.WoolGames} />,
"blitz": <BlitzStats stats={stats.Blitz} />,
"arcade": <ArcadeStats stats={stats.Arcade} />
} as const
const defaultOrder = Object.keys(statsComponents).sort()
const orderToUse = layout || defaultOrder
const [statsOrder, setStatsOrder] = useState<string[]>(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 (
<div className="mx-auto w-full lg:mx-0 lg:w-3/4 max-w-120 md:max-w-7/10">
<Accordion type="multiple" className="space-y-4">
{orderToUse.map((statKey) => (
<div key={statKey}>
{statsComponents[statKey as keyof typeof statsComponents]}
</div>
))}
</Accordion>
</div>
)
}
return (
<div className="mx-auto w-full lg:mx-0 lg:w-3/4 max-w-120 md:max-w-7/10">
<Accordion type="multiple" className="space-y-4">
<BedwarsStats stats={stats.Bedwars} />
<SkyWarsStats
stats={stats.SkyWars}
achievements_skywars_opal_obsession={achievements?.["skywars_opal_obsession"] ?? 0}
/>
<DuelsStats stats={stats.Duels} />
<MurderMysteryStats stats={stats.MurderMystery} />
<BuildBattleStats stats={stats.BuildBattle} />
<UHCStats stats={stats.UHC} />
<PitStats stats={stats.Pit} />
<TNTGamesStats stats={stats.TNTGames} />
<MegaWallsStats stats={stats.MegaWalls} />
<CopsAndCrimsStats stats={stats.CopsAndCrims} />
<WoolGamesStats stats={stats.WoolGames} />
<BlitzStats stats={stats.Blitz} />
<ArcadeStats stats={stats.Arcade} />
</Accordion>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={statsOrder} strategy={verticalListSortingStrategy}>
<Accordion type="multiple" className="space-y-4">
{statsOrder.map((statKey) => (
<SortableStatItem key={statKey} id={statKey}>
{statsComponents[statKey as keyof typeof statsComponents]}
</SortableStatItem>
))}
</Accordion>
</SortableContext>
</DndContext>
</div>
)
}

View File

@@ -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<PageProps<"/player/[ign]">, "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<PageProps<"/player/[ign]">, "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 (
<div className="flex flex-col items-center pb-5">
<DisplayName
@@ -101,7 +107,7 @@ async function SuspendedPage({ params }: Pick<PageProps<"/player/[ign]">, "param
session={session}
/>
{player.stats !== undefined ?
<PlayerStats stats={player.stats} achievements={player.achievements} /> :
<PlayerStats stats={player.stats} achievements={player.achievements} layout={layout} /> :
(
<div className="w-3/4">
<Card>