tierlist done(without sortables)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { sse } from "@/lib/utils";
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ ver: string }> },
|
||||
) {
|
||||
const { ver } = await params;
|
||||
return sse.new(`tl-${ver}`);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { tierlistStates } from "@/lib/db/schema";
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ ver: string }> },
|
||||
) {
|
||||
const { ver } = await params;
|
||||
const states = await db
|
||||
.select()
|
||||
.from(tierlistStates)
|
||||
.where(eq(tierlistStates.list, ver));
|
||||
return NextResponse.json(states);
|
||||
}
|
||||
+1
-1
@@ -47,7 +47,7 @@ export default function RootLayout({
|
||||
className="z-[-1] opacity-20 object-cover fixed top-0 left-0 w-full h-full"
|
||||
/>
|
||||
<Providers>{children}</Providers>
|
||||
<Toaster />
|
||||
<Toaster theme="dark" />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+84
-8
@@ -1,15 +1,91 @@
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Banner from "#/banner.webp";
|
||||
import ArtifactLogo from "#/logos/artifact.webp";
|
||||
import TierlistLogo from "#/logos/tierlist.webp";
|
||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/components/ui/item";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<Link href="/artifact">
|
||||
<Button>Artifact</Button>
|
||||
</Link>
|
||||
<Link href="/tl">
|
||||
<Button>TierList</Button>
|
||||
</Link>
|
||||
<div className="relative h-screen w-full overflow-hidden flex items-center justify-center">
|
||||
<div className="max-w-[85rem] w-full mx-auto px-4 sm:px-6 py-6">
|
||||
<Card className="max-w-lg w-full mx-auto pt-0">
|
||||
<Image
|
||||
src={Banner}
|
||||
alt="Banner"
|
||||
className="w-full rounded-t-xl"
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Item variant="outline" asChild>
|
||||
<Link href="/artifact">
|
||||
<ItemMedia>
|
||||
<Image
|
||||
src={ArtifactLogo}
|
||||
alt="เสือกไอดีชาวบ้าน"
|
||||
className="w-22"
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>เสือกไอดีชาวบ้าน</ItemTitle>
|
||||
<ItemDescription className="line-clamp-none">
|
||||
ใครอยากให้ดูอาร์ติแฟกต์ให้ในไลฟ์ ไปกรอกแบบฟอร์มได้เลย
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</ItemActions>
|
||||
</Link>
|
||||
</Item>
|
||||
|
||||
<Item variant="outline" asChild>
|
||||
<Link href="/tl">
|
||||
<ItemMedia>
|
||||
<Image
|
||||
src={TierlistLogo}
|
||||
alt="จัดเทียร์ลิสต์อบิส"
|
||||
className="w-22"
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>จัดเทียร์ลิสต์</ItemTitle>
|
||||
<ItemDescription className="line-clamp-none">
|
||||
เทียร์ลิสต์เมต้าตัวละครของอบิสแพทช์นั้นๆเท่านั้น
|
||||
ซึ่งจะเปลี่ยนไปในแต่ละแพทช์ตามความสามารถของตัวละครในการรับมือกับอบิสแพทช์นั้นๆ
|
||||
ไม่ใช่เทียร์ลิสต์ภาพรวมของตัวละคร
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</ItemActions>
|
||||
</Link>
|
||||
</Item>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
buzz.gunshiz.top is not affiliated with HoYoverse. Game content
|
||||
and materials are trademarks and copyrights of HoYoverse.
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
characters,
|
||||
tierlistBadges,
|
||||
tierlistColumns,
|
||||
tierlistTiers,
|
||||
tierlistTypes,
|
||||
tierlistVersions,
|
||||
versions,
|
||||
} from "@/lib/db/schema";
|
||||
import { TierList } from "../tierlist";
|
||||
|
||||
export default async function TierlistPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ type: string; ver: string }>;
|
||||
}) {
|
||||
const { type, ver } = await params;
|
||||
const config = await db.transaction(async (tx) => {
|
||||
// resolve version info
|
||||
const [version] = await tx
|
||||
.select()
|
||||
.from(tierlistVersions)
|
||||
.where(
|
||||
and(eq(tierlistVersions.type, type), eq(tierlistVersions.id, ver)),
|
||||
);
|
||||
if (!version) return;
|
||||
const [typeInfo] = await tx
|
||||
.select()
|
||||
.from(tierlistTypes)
|
||||
.where(eq(tierlistTypes.id, type));
|
||||
|
||||
// resolve `from` fields
|
||||
const vers = await tx
|
||||
.select({ id: versions.id, from: versions.from })
|
||||
.from(versions);
|
||||
const ids: string[] = [];
|
||||
let cur: string | null = version.from;
|
||||
while (cur) {
|
||||
ids.push(cur);
|
||||
const r = vers.find((v) => v.id === cur);
|
||||
cur = r?.from ?? null;
|
||||
}
|
||||
|
||||
const chars =
|
||||
ids.length > 0
|
||||
? await tx
|
||||
.select()
|
||||
.from(characters)
|
||||
.where(inArray(characters.version, ids))
|
||||
.orderBy(characters.order)
|
||||
: ([] as (typeof characters.$inferSelect)[]);
|
||||
|
||||
// fetch rows, columns, badges data...
|
||||
const tiers = await tx
|
||||
.select()
|
||||
.from(tierlistTiers)
|
||||
.orderBy(tierlistTiers.order);
|
||||
const columns = await tx
|
||||
.select()
|
||||
.from(tierlistColumns)
|
||||
.orderBy(tierlistColumns.order);
|
||||
const badgesList = await tx
|
||||
.select()
|
||||
.from(tierlistBadges)
|
||||
.orderBy(tierlistBadges.order);
|
||||
const badges = badgesList.map((b) => ({
|
||||
...b,
|
||||
tier: tiers.filter((t) => t.badges?.includes(b.id)).map((t) => t.id),
|
||||
}));
|
||||
|
||||
return { type: typeInfo, version, tiers, columns, badges, chars };
|
||||
});
|
||||
// console.log(config);
|
||||
if (!config) notFound();
|
||||
return <TierList editable {...config} />;
|
||||
// return <pre>{JSON.stringify(config, null, 2)}</pre>;
|
||||
}
|
||||
@@ -2,18 +2,32 @@
|
||||
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
import Image from "next/image";
|
||||
import { useContext, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import AmberIcon from "#/amber.png";
|
||||
import { SimpleTooltip } from "@/components/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { characters } from "@/lib/db/schema";
|
||||
import type { characters, tierlistStates } from "@/lib/db/schema";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TierListContext } from "./context";
|
||||
|
||||
export function Draggable({ char }: { char: typeof characters.$inferSelect }) {
|
||||
const { tileSize, badgeSize } = useContext(TierListContext);
|
||||
export function Draggable({
|
||||
char,
|
||||
state,
|
||||
tier,
|
||||
}: {
|
||||
char: typeof characters.$inferSelect;
|
||||
state?: typeof tierlistStates.$inferInsert;
|
||||
tier?: string;
|
||||
}) {
|
||||
const { tileSize, badgeSize, badges, setState, editable } =
|
||||
useContext(TierListContext);
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: char.id,
|
||||
@@ -26,12 +40,41 @@ export function Draggable({ char }: { char: typeof characters.$inferSelect }) {
|
||||
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
|
||||
const [comment, setComment] = useState("");
|
||||
const [assignedBadges, setBadges] = useState<string[]>([]);
|
||||
const [dirty, setDirty] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (dirty) return;
|
||||
setComment(state?.comment || "");
|
||||
setBadges(state?.badges || []);
|
||||
}, [dirty, state]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (!dirty) return;
|
||||
setDirty(0);
|
||||
setState(char.id, { comment, badges: assignedBadges });
|
||||
}, dirty);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [dirty, comment, setState, char, assignedBadges]);
|
||||
|
||||
function toggleBadge(badge: string) {
|
||||
setDirty(200);
|
||||
setBadges(
|
||||
assignedBadges.includes(badge)
|
||||
? assignedBadges.filter((b) => b !== badge)
|
||||
: [...assignedBadges, badge],
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={panelOpen} onOpenChange={setPanelOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
suppressHydrationWarning
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
onContextMenu={(ev) => {
|
||||
@@ -39,49 +82,124 @@ export function Draggable({ char }: { char: typeof characters.$inferSelect }) {
|
||||
setPanelOpen((x) => !x);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={`/cdn/${char.image}`}
|
||||
style={{
|
||||
background: `rgba(${
|
||||
char.stars === 5
|
||||
? "200,124,36"
|
||||
: char.stars === 4
|
||||
? "148,112,187"
|
||||
: "100,100,100"
|
||||
}) linear-gradient(136deg,rgba(49,43,71,.5294117647058824),transparent)`,
|
||||
}}
|
||||
className="rounded"
|
||||
alt={char.name}
|
||||
height={tileSize}
|
||||
width={tileSize}
|
||||
/>
|
||||
<div className="relative">
|
||||
<SimpleTooltip text={char.name} delayDuration={1000}>
|
||||
<Image
|
||||
src={`/cdn/${char.image}`}
|
||||
style={{
|
||||
background: `rgba(${
|
||||
char.stars === 5
|
||||
? "200,124,36"
|
||||
: char.stars === 4
|
||||
? "148,112,187"
|
||||
: "100,100,100"
|
||||
}) linear-gradient(136deg,rgba(49,43,71,.5294117647058824),transparent)`,
|
||||
}}
|
||||
className="rounded hover:brightness-110"
|
||||
alt={char.name}
|
||||
height={tileSize}
|
||||
width={tileSize}
|
||||
/>
|
||||
</SimpleTooltip>
|
||||
{[
|
||||
"bottom-0.5 right-0.5",
|
||||
"bottom-0.5 left-0.5",
|
||||
"top-0.5 right-0.5",
|
||||
"top-0.5 left-0.5",
|
||||
].map((p, i) => {
|
||||
if (!assignedBadges[i]) return "";
|
||||
const badge = badges.find((e) => e.id === assignedBadges[i]);
|
||||
if (!badge) return "";
|
||||
return badge.image ? (
|
||||
<Image
|
||||
key={p}
|
||||
src={`/cdn/${badge.image}`}
|
||||
className={`bg-[#2225] backdrop-blur-sm ${p} absolute rounded border`}
|
||||
alt="badge"
|
||||
width={badgeSize}
|
||||
height={badgeSize}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
key={p}
|
||||
style={{ width: badgeSize, height: badgeSize }}
|
||||
className={`bg-[#2225] backdrop-blur-sm ${p} absolute rounded border font-bold`}
|
||||
>
|
||||
{badge.name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" className="bg-[#2225] backdrop-blur-md p-1">
|
||||
<div className="flex gap-2">
|
||||
<div className="grid grid-cols-2 gap-1 border border-dashed rounded p-1">
|
||||
{Array(12)
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
id: i,
|
||||
name: "A+",
|
||||
image: "0199946b-666c-717f-a42b-c4ca067c3e39",
|
||||
order: i,
|
||||
}))
|
||||
.map((e) => (
|
||||
<Image
|
||||
src={`/cdn/${e.image}`}
|
||||
alt={e.name}
|
||||
key={`B${e.id}`}
|
||||
width={48}
|
||||
height={48}
|
||||
className="border rounded hover:brightness-110"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{editable && (
|
||||
<div className="flex flex-col justify-between shrink-0 gap-2">
|
||||
<div className="grid grid-cols-2 gap-1 h-min items-stretch">
|
||||
{badges
|
||||
.filter(
|
||||
(b) =>
|
||||
assignedBadges.includes(b.id) ||
|
||||
!b.tier.length ||
|
||||
!tier ||
|
||||
b.tier.includes(tier),
|
||||
)
|
||||
.map((e) =>
|
||||
e.image ? (
|
||||
<Image
|
||||
src={`/cdn/${e.image}`}
|
||||
alt={e.name}
|
||||
key={`B${e.id}`}
|
||||
width={32}
|
||||
height={32}
|
||||
className={cn(
|
||||
"border rounded hover:brightness-110 hover:backdrop-brightness-300 cursor-pointer",
|
||||
assignedBadges.includes(e.id) &&
|
||||
"backdrop-brightness-500",
|
||||
)}
|
||||
onClick={() => toggleBadge(e.id)}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
key={`B${e.id}`}
|
||||
className={cn(
|
||||
"border rounded hover:brightness-110 hover:backdrop-brightness-300 flex justify-center items-center font-bold cursor-pointer min-w-8 min-h-8",
|
||||
assignedBadges.includes(e.id) &&
|
||||
"backdrop-brightness-500",
|
||||
)}
|
||||
onClick={() => toggleBadge(e.id)}
|
||||
type="button"
|
||||
>
|
||||
{e.name}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href={`https://gi.yatta.moe/en/archive/avatar/${char.amber}/${char.name.replace(/ /g, "-").toLowerCase()}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<Button variant="outline" className="p-1 w-full">
|
||||
<Image src={AmberIcon} alt="Amber" width={16} height={16} />
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="text-[8px]">เปิดใน</span>
|
||||
<span className="text-[10px]">Amber</span>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
placeholder="Comment..."
|
||||
className="aspect-square bg-card resize-none"
|
||||
value={comment}
|
||||
onChange={(ev) => {
|
||||
setDirty(500);
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
disabled={!editable}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
"use client";
|
||||
import { createContext } from "react";
|
||||
import type { characters, tierlistBadges } from "@/lib/db/schema";
|
||||
import type { characters, tierlistBadges, tierlistStates } from "@/lib/db/schema";
|
||||
|
||||
export const TierListContext = createContext<{
|
||||
chars: (typeof characters.$inferSelect)[];
|
||||
badges: (typeof tierlistBadges.$inferSelect)[];
|
||||
badges: (typeof tierlistBadges.$inferSelect & { tier: string[] })[];
|
||||
tileSize: number;
|
||||
badgeSize: number;
|
||||
setState: (char: string, data: Partial<typeof tierlistStates.$inferInsert>) => void;
|
||||
editable: boolean;
|
||||
}>({
|
||||
chars: [],
|
||||
badges: [],
|
||||
tileSize: 64,
|
||||
badgeSize: 24,
|
||||
setState: () => {},
|
||||
editable: false
|
||||
});
|
||||
|
||||
@@ -49,7 +49,8 @@ export default async function TierlistPage({
|
||||
? await tx
|
||||
.select()
|
||||
.from(characters)
|
||||
.where(inArray(characters.version, ids)).orderBy(characters.order)
|
||||
.where(inArray(characters.version, ids))
|
||||
.orderBy(characters.order)
|
||||
: ([] as (typeof characters.$inferSelect)[]);
|
||||
|
||||
// fetch rows, columns, badges data...
|
||||
@@ -61,10 +62,14 @@ export default async function TierlistPage({
|
||||
.select()
|
||||
.from(tierlistColumns)
|
||||
.orderBy(tierlistColumns.order);
|
||||
const badges = await tx
|
||||
const badgesList = await tx
|
||||
.select()
|
||||
.from(tierlistBadges)
|
||||
.orderBy(tierlistBadges.order);
|
||||
const badges = badgesList.map((b) => ({
|
||||
...b,
|
||||
tier: tiers.filter((t) => t.badges?.includes(b.id)).map((t) => t.id),
|
||||
}));
|
||||
|
||||
return { type: typeInfo, version, tiers, columns, badges, chars };
|
||||
});
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
DragOverlay,
|
||||
useDroppable,
|
||||
} from "@dnd-kit/core";
|
||||
import { Calculator, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import {
|
||||
Calculator,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -14,10 +19,12 @@ import {
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import Favicon from "#/favicon.webp";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -35,10 +42,17 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { tlPlacements, tlState } from "@/lib/api";
|
||||
import type {
|
||||
characters,
|
||||
tierlistBadges,
|
||||
tierlistColumns,
|
||||
tierlistStates,
|
||||
tierlistTiers,
|
||||
tierlistTypes,
|
||||
tierlistVersions,
|
||||
@@ -46,12 +60,6 @@ import type {
|
||||
import { TierListCell } from "./cell";
|
||||
import { Draggable } from "./character";
|
||||
import { TierListContext } from "./context";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { SimpleTooltip } from "@/components/tooltip";
|
||||
|
||||
function Untiered({
|
||||
children,
|
||||
@@ -88,13 +96,15 @@ export function TierList({
|
||||
tiers,
|
||||
columns,
|
||||
badges,
|
||||
editable = false,
|
||||
}: {
|
||||
type: typeof tierlistTypes.$inferSelect;
|
||||
version: typeof tierlistVersions.$inferSelect;
|
||||
chars: (typeof characters.$inferSelect)[];
|
||||
tiers: (typeof tierlistTiers.$inferSelect)[];
|
||||
columns: (typeof tierlistColumns.$inferSelect)[];
|
||||
badges: (typeof tierlistBadges.$inferSelect)[];
|
||||
badges: (typeof tierlistBadges.$inferSelect & { tier: string[] })[];
|
||||
editable?: boolean;
|
||||
}) {
|
||||
const [tileSizeSetting, setTileSize] = useLocalStorage<number | null>(
|
||||
"tl_tileSize",
|
||||
@@ -103,6 +113,45 @@ export function TierList({
|
||||
const [badgeSize, setBadgeSize] = useLocalStorage<number>("tl_badgeSize", 24);
|
||||
const [tileSizeAuto, setTileSizeAuto] = useState(0);
|
||||
const tileSize = tileSizeSetting || tileSizeAuto;
|
||||
const [states, setStates] = useState<(typeof tierlistStates.$inferSelect)[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
//#region error handling
|
||||
useEffect(() => {
|
||||
const handler = (ev: ErrorEvent) =>
|
||||
toast("Unexpected Error Occured", {
|
||||
icon: <TriangleAlert className="text-red-400" />,
|
||||
description: `${ev.error.message || ev.error || "*unknown error*"}`,
|
||||
});
|
||||
window.addEventListener("error", handler);
|
||||
return () => window.removeEventListener("error", handler);
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region states fetching
|
||||
useEffect(() => {
|
||||
async function fetchStates() {
|
||||
setStates(
|
||||
await fetch(`/api/tl/${version.id}/states`).then((r) => r.json()),
|
||||
);
|
||||
}
|
||||
fetchStates();
|
||||
}, [version]);
|
||||
|
||||
useEffect(() => {
|
||||
const es = new EventSource(`/api/tl/${version.id}/ev`);
|
||||
es.addEventListener("update_states", (d) => setStates(JSON.parse(d.data)));
|
||||
es.addEventListener("update_placements", (d) =>
|
||||
setPlacements(JSON.parse(d.data)),
|
||||
);
|
||||
es.onerror = () =>
|
||||
toast("Live Reload ใช้งานไม่ได้ ลองรีโหลดใหม่", {
|
||||
icon: <TriangleAlert className="text-red-400" />,
|
||||
});
|
||||
return () => es.close();
|
||||
}, [version]);
|
||||
//#endregion
|
||||
|
||||
const colRef = useRef<HTMLDivElement | null>(null);
|
||||
const untieredRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -161,23 +210,23 @@ export function TierList({
|
||||
true,
|
||||
);
|
||||
|
||||
//#region placements handling
|
||||
const initialPlacements: Record<string, string[]> = {
|
||||
untiered: chars.map((ch) => ch.id),
|
||||
const tiered: (string | undefined)[] = Object.values({
|
||||
...version.placements,
|
||||
untiered: undefined,
|
||||
}).flat();
|
||||
const [placements, setPlacements] = useState<Record<string, string[]>>({
|
||||
...Object.fromEntries(
|
||||
tiers.flatMap((t) =>
|
||||
columns.map((c) => [`${t.id}-${c.id}`, [] as string[]]),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
const [placements, setPlacements] = useLocalStorage<Record<string, string[]>>(
|
||||
`tl_${type.id}_${version.id}_placements`,
|
||||
initialPlacements,
|
||||
);
|
||||
...version.placements,
|
||||
untiered: chars.map((ch) => ch.id).filter((c) => !tiered.includes(c)),
|
||||
});
|
||||
const [dragging, setDragging] = useState<string | null>(null);
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
if (!editable) return;
|
||||
const { active, over } = event;
|
||||
if (!over) return;
|
||||
const charId = active.id as string;
|
||||
@@ -190,16 +239,46 @@ export function TierList({
|
||||
}
|
||||
}
|
||||
if (!source || source === target) return;
|
||||
setPlacements({
|
||||
const newPlacements = {
|
||||
...placements,
|
||||
[source]: placements[source].filter((id) => id !== charId),
|
||||
[target]: [...placements[target], charId],
|
||||
});
|
||||
};
|
||||
setPlacements(newPlacements);
|
||||
tlPlacements(version.id, newPlacements).catch((e) =>
|
||||
toast(`Sync ล้มเหลว: ${e.message || e}`, {
|
||||
icon: <TriangleAlert className="text-red-400" />,
|
||||
}),
|
||||
);
|
||||
}
|
||||
//#endregion placements
|
||||
|
||||
return (
|
||||
<TierListContext.Provider value={{ badges, chars, tileSize, badgeSize }}>
|
||||
<TierListContext.Provider
|
||||
value={{
|
||||
badges,
|
||||
chars,
|
||||
tileSize,
|
||||
badgeSize,
|
||||
editable,
|
||||
async setState(char, data) {
|
||||
const newEntry = {
|
||||
...states.find((e) => e.char === char),
|
||||
...(Object.fromEntries(
|
||||
Object.entries(data).filter(([_, v]) => v !== undefined),
|
||||
) as typeof tierlistStates.$inferSelect),
|
||||
list: version.id,
|
||||
char,
|
||||
};
|
||||
setStates((s) => [...s.filter((e) => e.char !== char), newEntry]);
|
||||
tlState(newEntry).catch((e) =>
|
||||
toast(`Sync ล้มเหลว: ${e.message || e}`, {
|
||||
icon: <TriangleAlert className="text-red-400" />,
|
||||
}),
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DndContext
|
||||
onDragStart={(e) => setDragging(e.active.id as string)}
|
||||
onDragEnd={handleDragEnd}
|
||||
@@ -385,7 +464,12 @@ export function TierList({
|
||||
return (
|
||||
<TierListCell key={cellId} column={c} tier={t}>
|
||||
{cellChars.map((ch) => (
|
||||
<Draggable key={ch.id} char={ch} />
|
||||
<Draggable
|
||||
key={ch.id}
|
||||
char={ch}
|
||||
tier={t.id}
|
||||
state={states.find((s) => s.char === ch.id)}
|
||||
/>
|
||||
))}
|
||||
</TierListCell>
|
||||
);
|
||||
@@ -411,7 +495,13 @@ export function TierList({
|
||||
{untieredOpen &&
|
||||
placements.untiered.map((id) => {
|
||||
const ch = chars.find((c) => c.id === id)!;
|
||||
return <Draggable key={ch.id} char={ch} />;
|
||||
return (
|
||||
<Draggable
|
||||
key={ch.id}
|
||||
char={ch}
|
||||
state={states.find((s) => s.char === ch.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Untiered>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user