feat(tl): ability to add/remove characters
This commit is contained in:
@@ -304,20 +304,20 @@ export function SlipButton() {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={() => {
|
||||
window.location.href = "/rubgram/admin/slip";
|
||||
}}
|
||||
>
|
||||
<Logs size={24} className="size-6" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>All time คิว</p>
|
||||
</TooltipContent>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={() => {
|
||||
window.location.href = "/rubgram/admin/slip";
|
||||
}}
|
||||
>
|
||||
<Logs size={24} className="size-6" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>All time คิว</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Trash } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import AmberIcon from "#/amber.png";
|
||||
@@ -21,13 +22,22 @@ export function Draggable({
|
||||
char,
|
||||
state,
|
||||
tier,
|
||||
cid,
|
||||
}: {
|
||||
char: typeof characters.$inferSelect;
|
||||
state?: typeof tierlistStates.$inferInsert;
|
||||
tier?: string;
|
||||
cid?: string;
|
||||
}) {
|
||||
const { tileSize, badgeSize, badges, setState, editable } =
|
||||
useContext(TierListContext);
|
||||
const {
|
||||
tileSize,
|
||||
badgeSize,
|
||||
badges,
|
||||
setState,
|
||||
editable,
|
||||
deleteMode,
|
||||
removeChar,
|
||||
} = useContext(TierListContext);
|
||||
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
|
||||
@@ -43,12 +53,12 @@ export function Draggable({
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (!dirty) return;
|
||||
if (!dirty || !cid) return;
|
||||
setDirty(0);
|
||||
setState(char.id, { comment, badges: assignedBadges });
|
||||
setState(cid, { comment, badges: assignedBadges });
|
||||
}, dirty);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [dirty, comment, setState, char, assignedBadges]);
|
||||
}, [dirty, comment, setState, cid, assignedBadges]);
|
||||
|
||||
function toggleBadge(badge: string) {
|
||||
setDirty(200);
|
||||
@@ -60,152 +70,192 @@ export function Draggable({
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableDraggable id={char.id}>
|
||||
{({ listeners, attributes, setNodeRef, style }) => (
|
||||
<Popover open={panelOpen} onOpenChange={setPanelOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
suppressHydrationWarning
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
onContextMenu={(ev) => {
|
||||
ev.preventDefault();
|
||||
setPanelOpen((x) => !x);
|
||||
}}
|
||||
>
|
||||
<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-[#2228] ${p} absolute rounded border`}
|
||||
alt="badge"
|
||||
width={badgeSize}
|
||||
height={badgeSize}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
key={p}
|
||||
style={{ width: badgeSize, height: badgeSize }}
|
||||
className={`bg-[#2228] ${p} absolute rounded border font-bold`}
|
||||
>
|
||||
{badge.name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
className="bg-[#2225] backdrop-blur-md p-1"
|
||||
<SortableDraggable id={cid || char.id}>
|
||||
{({ listeners, attributes, setNodeRef, style }) =>
|
||||
deleteMode ? (
|
||||
<button
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
suppressHydrationWarning
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
onContextMenu={(ev) => {
|
||||
ev.preventDefault();
|
||||
if (cid) removeChar(cid);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
{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 className="relative">
|
||||
<SimpleTooltip text="คลิ๊กขวาเพื่อลบ">
|
||||
<div className="absolute justify-center items-center top-0 left-0 bottom-0 right-0 rounded bg-[#0005] flex opacity-0 hover:opacity-100 transition-opacity">
|
||||
<Trash />
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
placeholder="Comment..."
|
||||
className="aspect-square bg-card resize-none disabled:opacity-90"
|
||||
value={comment}
|
||||
onChange={(ev) => {
|
||||
setDirty(500);
|
||||
setComment(ev.target.value);
|
||||
</SimpleTooltip>
|
||||
<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)`,
|
||||
}}
|
||||
disabled={!editable}
|
||||
className="rounded hover:brightness-110"
|
||||
alt={char.name}
|
||||
height={tileSize}
|
||||
width={tileSize}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<Popover open={panelOpen} onOpenChange={setPanelOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
suppressHydrationWarning
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
onContextMenu={(ev) => {
|
||||
ev.preventDefault();
|
||||
setPanelOpen((x) => !x);
|
||||
}}
|
||||
>
|
||||
<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-[#2228] ${p} absolute rounded border`}
|
||||
alt="badge"
|
||||
width={badgeSize}
|
||||
height={badgeSize}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
key={p}
|
||||
style={{ width: badgeSize, height: badgeSize }}
|
||||
className={`bg-[#2228] ${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">
|
||||
{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 disabled:opacity-90"
|
||||
value={comment}
|
||||
onChange={(ev) => {
|
||||
setDirty(500);
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
disabled={!editable}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
</SortableDraggable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export const TierListContext = createContext<{
|
||||
data: Partial<typeof tierlistStates.$inferInsert>,
|
||||
) => void;
|
||||
editable: boolean;
|
||||
deleteMode: boolean;
|
||||
removeChar: (cid: string) => void;
|
||||
}>({
|
||||
chars: [],
|
||||
badges: [],
|
||||
@@ -23,4 +25,6 @@ export const TierListContext = createContext<{
|
||||
badgeSize: 24,
|
||||
setState: () => {},
|
||||
editable: false,
|
||||
deleteMode: false,
|
||||
removeChar: () => {},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/a11y/noStaticElementInteractions: i dont know */
|
||||
"use client";
|
||||
|
||||
import {
|
||||
@@ -7,7 +8,18 @@ import {
|
||||
useDroppable,
|
||||
} from "@dnd-kit/core";
|
||||
import { rectSortingStrategy, SortableContext } from "@dnd-kit/sortable";
|
||||
import { Calculator, ChevronDown, ChevronUp, X, Settings, Home, Settings2, FileQuestionMark } from "lucide-react";
|
||||
import {
|
||||
Calculator,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
CopyPlus,
|
||||
FileQuestionMark,
|
||||
Home,
|
||||
Settings,
|
||||
Settings2,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Fragment,
|
||||
@@ -26,7 +38,9 @@ import Qiqi from "#/assets/qiqi.webp";
|
||||
import BadgeSP from "#/assets/sp.webp";
|
||||
import BadgeSSS from "#/assets/sss.webp";
|
||||
import { Blocker } from "@/components/blocker";
|
||||
import { ComboBox } from "@/components/combobox";
|
||||
import Image from "@/components/image";
|
||||
import { SimpleTooltip } from "@/components/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -58,6 +72,7 @@ import type {
|
||||
tierlistTypes,
|
||||
tierlistVersions,
|
||||
} from "@/lib/db/schema";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TierListCell } from "./cell";
|
||||
import { Draggable } from "./character";
|
||||
import { TierListContext } from "./context";
|
||||
@@ -114,6 +129,7 @@ export function TierList({
|
||||
const [badgeSize, setBadgeSize] = useLocalStorage<number>("tl_badgeSize", 24);
|
||||
const [tileSizeAuto, setTileSizeAuto] = useState(0);
|
||||
const tileSize = tileSizeSetting || tileSizeAuto;
|
||||
const [deleteMode, setDeleteMode] = shared.state("tl.deleteMode");
|
||||
const [states, setStates] = useState<(typeof tierlistStates.$inferSelect)[]>(
|
||||
[],
|
||||
);
|
||||
@@ -130,7 +146,12 @@ export function TierList({
|
||||
toast.error("Unexpected Error Occured", {
|
||||
description: `${ev?.error?.message || ev?.error || "*unknown error*"}`,
|
||||
});
|
||||
const promiseHandler = (ev: PromiseRejectionEvent) =>
|
||||
toast.error("Unexpected Error Occured", {
|
||||
description: `${ev.reason || ev || "*unknown error*"}`,
|
||||
});
|
||||
window.addEventListener("error", handler);
|
||||
window.addEventListener("unhandledrejection", promiseHandler);
|
||||
return () => window.removeEventListener("error", handler);
|
||||
});
|
||||
//#endregion
|
||||
@@ -256,8 +277,8 @@ export function TierList({
|
||||
|
||||
let newPlacements = { ...placements };
|
||||
|
||||
// Check if dropped on another character (sorting within same cell)
|
||||
if (chars.some((ch) => ch.id === targetId)) {
|
||||
if (chars.some((ch) => ch.id === targetId.split("#")[0])) {
|
||||
// Check if dropped on another character (sorting within same cell)
|
||||
// Find target cell
|
||||
let targetCell: string | undefined;
|
||||
for (const cell in placements) {
|
||||
@@ -322,20 +343,32 @@ export function TierList({
|
||||
tileSize,
|
||||
badgeSize,
|
||||
editable,
|
||||
async setState(char, data) {
|
||||
deleteMode: !!deleteMode,
|
||||
async setState(ref, data) {
|
||||
const newEntry = {
|
||||
...states.find((e) => e.char === char),
|
||||
...states.find((e) => e.ref === ref),
|
||||
...(Object.fromEntries(
|
||||
Object.entries(data).filter(([_, v]) => v !== undefined),
|
||||
) as typeof tierlistStates.$inferSelect),
|
||||
list: version.id,
|
||||
char,
|
||||
char: ref.split("#")[0],
|
||||
ref,
|
||||
};
|
||||
setStates((s) => [...s.filter((e) => e.char !== char), newEntry]);
|
||||
setStates((s) => [...s.filter((e) => e.ref !== ref), newEntry]);
|
||||
tlState(newEntry).catch((e) =>
|
||||
toast.error(`Sync ล้มเหลว: ${e.message || e}`),
|
||||
);
|
||||
},
|
||||
async removeChar(cid: string) {
|
||||
setPlacements((s) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(s).map(([k, v]) => [
|
||||
k,
|
||||
v.filter((id) => id !== cid),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DndContext
|
||||
@@ -381,146 +414,159 @@ export function TierList({
|
||||
>
|
||||
<div
|
||||
className="bg-[#0005] text-center font-semibold py-1 relative"
|
||||
style={{ gridColumn: '1 / -1' }}
|
||||
style={{ gridColumn: "1 / -1" }}
|
||||
>
|
||||
<span>{type.name} เวอร์ชั่น {version.name} ระดับ
|
||||
<span className="text-yellow-400">{type.mode}</span> ใช้ได้ถึง </span>
|
||||
<span>
|
||||
{type.name} เวอร์ชั่น {version.name} ระดับ
|
||||
<span className="text-yellow-400">{type.mode}</span> ใช้ได้ถึง{" "}
|
||||
</span>
|
||||
<span className="text-green-400">{version.deprecates}</span>
|
||||
</div>
|
||||
<div className="grid place-items-center bg-[#2225] cursor-pointer">
|
||||
<Dialog>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Settings className="size-8" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<Link href="/tl">
|
||||
<DropdownMenuItem> <Home className="size-4" />หน้าหลัก</DropdownMenuItem>
|
||||
</Link>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem> <Settings2 className="size-4" />การตั้งค่า</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DropdownMenuItem onClick={() => showDisclaimer(true)}>
|
||||
<FileQuestionMark className="size-4" />แสดงเงื่อนไข
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>การตั้งค่า</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="grid gap-2 group">
|
||||
<span className="flex gap-1">
|
||||
ขนาดตัวละคร{" "}
|
||||
<span className="flex gap-1 text-muted-foreground md:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
(px){" "}
|
||||
{tileSizeSetting ? (
|
||||
""
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Calculator className="text-emerald-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>คำนวณอัตโนมัติ</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Settings className="size-8" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<Link href="/tl">
|
||||
<DropdownMenuItem>
|
||||
{" "}
|
||||
<Home className="size-4" />
|
||||
หน้าหลัก
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem>
|
||||
{" "}
|
||||
<Settings2 className="size-4" />
|
||||
การตั้งค่า
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DropdownMenuItem onClick={() => showDisclaimer(true)}>
|
||||
<FileQuestionMark className="size-4" />
|
||||
แสดงเงื่อนไข
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>การตั้งค่า</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="grid gap-2 group">
|
||||
<span className="flex gap-1">
|
||||
ขนาดตัวละคร{" "}
|
||||
<span className="flex gap-1 text-muted-foreground md:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
(px){" "}
|
||||
{tileSizeSetting ? (
|
||||
""
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Calculator className="text-emerald-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>คำนวณอัตโนมัติ</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={tileSizeSetting || Math.round(tileSizeAuto)}
|
||||
onChange={(ev) =>
|
||||
setTileSize(Number(ev.target.value))
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setTileSize((x) => (x || tileSize) + 1)
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronUp />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setTileSize((x) => (x || tileSize) - 1)
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={
|
||||
tileSizeSetting || Math.round(tileSizeAuto)
|
||||
}
|
||||
onChange={(ev) =>
|
||||
setTileSize(Number(ev.target.value))
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setTileSize((x) => (x || tileSize) + 1)
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronUp />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setTileSize((x) => (x || tileSize) - 1)
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2 group">
|
||||
<span>
|
||||
ขนาดเครื่องหมาย{" "}
|
||||
<span className="text-muted-foreground md:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
(px)
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={badgeSize}
|
||||
onChange={(ev) =>
|
||||
setBadgeSize(Number(ev.target.value))
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setBadgeSize((x) => (x || tileSize) + 1)
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronUp />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setBadgeSize((x) => (x || tileSize) - 1)
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2 group">
|
||||
<span>
|
||||
ขนาดเครื่องหมาย{" "}
|
||||
<span className="text-muted-foreground md:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
(px)
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={badgeSize}
|
||||
onChange={(ev) =>
|
||||
setBadgeSize(Number(ev.target.value))
|
||||
}
|
||||
type="number"
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"rgba(200,124,36) linear-gradient(136deg,rgba(49,43,71,.5294117647058824),transparent)",
|
||||
width: tileSize,
|
||||
}}
|
||||
className="rounded aspect-square relative"
|
||||
>
|
||||
<Image src={Qiqi} alt="Qiqi" fill />
|
||||
<Image
|
||||
className="bg-[#2225] backdrop-blur-sm bottom-0.5 left-0.5 absolute rounded border"
|
||||
src={BadgeSSS}
|
||||
alt="Test Badge"
|
||||
width={badgeSize}
|
||||
height={badgeSize}
|
||||
/>
|
||||
<Image
|
||||
className="bg-[#2225] backdrop-blur-sm bottom-0.5 right-0.5 absolute border rounded"
|
||||
src={BadgeSP}
|
||||
alt="Test Badge"
|
||||
width={badgeSize}
|
||||
height={badgeSize}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setBadgeSize((x) => (x || tileSize) + 1)
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronUp />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setBadgeSize((x) => (x || tileSize) - 1)
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"rgba(200,124,36) linear-gradient(136deg,rgba(49,43,71,.5294117647058824),transparent)",
|
||||
width: tileSize,
|
||||
}}
|
||||
className="rounded aspect-square relative"
|
||||
>
|
||||
<Image src={Qiqi} alt="Qiqi" fill />
|
||||
<Image
|
||||
className="bg-[#2225] backdrop-blur-sm bottom-0.5 left-0.5 absolute rounded border"
|
||||
src={BadgeSSS}
|
||||
alt="Test Badge"
|
||||
width={badgeSize}
|
||||
height={badgeSize}
|
||||
/>
|
||||
<Image
|
||||
className="bg-[#2225] backdrop-blur-sm bottom-0.5 right-0.5 absolute border rounded"
|
||||
src={BadgeSP}
|
||||
alt="Test Badge"
|
||||
width={badgeSize}
|
||||
height={badgeSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{columns.map((c) => (
|
||||
@@ -561,8 +607,14 @@ export function TierList({
|
||||
{columns.map((c) => {
|
||||
const cellId = `${t.id}-${c.id}`;
|
||||
const cellChars = placements[cellId]
|
||||
.map((id) => chars.find((ch) => ch.id === id))
|
||||
.filter((x): x is typeof characters.$inferSelect => !!x);
|
||||
.map((id) => [
|
||||
id,
|
||||
chars.find((ch) => ch.id === id.split("#")[0]),
|
||||
])
|
||||
.filter(
|
||||
(x): x is [string, typeof characters.$inferSelect] =>
|
||||
!!x[1],
|
||||
);
|
||||
return (
|
||||
<TierListCell
|
||||
key={cellId}
|
||||
@@ -570,12 +622,13 @@ export function TierList({
|
||||
tier={t}
|
||||
items={placements[cellId]}
|
||||
>
|
||||
{cellChars.map((ch) => (
|
||||
{cellChars.map(([id, ch]) => (
|
||||
<Draggable
|
||||
key={ch.id}
|
||||
key={id}
|
||||
cid={id}
|
||||
char={ch}
|
||||
tier={t.id}
|
||||
state={states.find((s) => s.char === ch.id)}
|
||||
state={states.find((s) => s.ref === id)}
|
||||
/>
|
||||
))}
|
||||
</TierListCell>
|
||||
@@ -589,13 +642,52 @@ export function TierList({
|
||||
<Button
|
||||
onClick={() => setUntieredOpen(!untieredOpen)}
|
||||
variant="outline"
|
||||
className="rounded-none"
|
||||
className="rounded-none flex"
|
||||
>
|
||||
({placements.untiered.length}) ตัวละครที่ไม่ได้อยู่ในเทียร์
|
||||
{untieredOpen ? (
|
||||
<ChevronDown className="ml-1" />
|
||||
) : (
|
||||
<ChevronUp className="ml-1" />
|
||||
{editable && untieredOpen && <div className="w-9 h-4" />}
|
||||
<span className="w-full flex gap-1 items-center justify-center">
|
||||
({placements.untiered.length}) ตัวละครที่ไม่ได้อยู่ในเทียร์
|
||||
{untieredOpen ? (
|
||||
<ChevronDown className="ml-1" />
|
||||
) : (
|
||||
<ChevronUp className="ml-1" />
|
||||
)}
|
||||
</span>
|
||||
{editable && untieredOpen && (
|
||||
<div
|
||||
className="flex gap-1 items-center justify-center"
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
>
|
||||
<ComboBox
|
||||
placeholder="ค้นหาตัวละคร"
|
||||
id="character"
|
||||
name="character"
|
||||
data={chars.map((c) => ({ label: c.name, value: c.id }))}
|
||||
className="w-full bg-transparent! hover:bg-accent!"
|
||||
trigger={
|
||||
<CopyPlus className="text-emerald-400 pointer-events-auto!" />
|
||||
}
|
||||
onValueSelect={(v) => {
|
||||
setPlacements((x) => ({
|
||||
...x,
|
||||
untiered: [`${v}#${Date.now()}`, ...x.untiered],
|
||||
}));
|
||||
toast.success(`เพิ่ม ${v} เข้าเทียร์ลิสต์แล้้ว`);
|
||||
}}
|
||||
/>
|
||||
<SimpleTooltip text="ลบตัวละครออก">
|
||||
<Trash2
|
||||
className={cn(
|
||||
"pointer-events-auto! text-red-500",
|
||||
deleteMode && "animate-pulse",
|
||||
)}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
setDeleteMode((x) => !x);
|
||||
}}
|
||||
/>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<SortableContext
|
||||
@@ -606,13 +698,15 @@ export function TierList({
|
||||
{untieredOpen &&
|
||||
placements.untiered
|
||||
.map((id) => {
|
||||
const ch = chars.find((c) => c.id === id);
|
||||
const cid = id.split("#")[0];
|
||||
const ch = chars.find((c) => c.id === cid);
|
||||
if (!ch) return null;
|
||||
return (
|
||||
<Draggable
|
||||
key={ch.id}
|
||||
key={id}
|
||||
cid={id}
|
||||
char={ch}
|
||||
state={states.find((s) => s.char === ch.id)}
|
||||
state={states.find((s) => s.ref === cid)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -623,7 +717,10 @@ export function TierList({
|
||||
</div>
|
||||
<DragOverlay>
|
||||
{dragging && (
|
||||
<Draggable char={chars.find((c) => c.id === dragging)!} />
|
||||
<Draggable
|
||||
char={chars.find((c) => c.id === dragging.split("#")[0])!}
|
||||
cid={dragging}
|
||||
/>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
|
||||
+21
-14
@@ -24,10 +24,14 @@ export function ComboBox({
|
||||
id,
|
||||
name,
|
||||
className,
|
||||
trigger,
|
||||
onValueSelect,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
data: { value: string; label: string }[];
|
||||
placeholder?: string;
|
||||
trigger?: React.ReactNode;
|
||||
onValueSelect?: (val: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, setValue] = React.useState("");
|
||||
@@ -36,20 +40,22 @@ export function ComboBox({
|
||||
<>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn("w-[200px] justify-between", className)}
|
||||
{...props}
|
||||
>
|
||||
{value
|
||||
? data.find((ent) => ent.value === value)?.label
|
||||
: placeholder}
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
{trigger || (
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn("w-50 justify-between", className)}
|
||||
{...props}
|
||||
>
|
||||
{value
|
||||
? data.find((ent) => ent.value === value)?.label
|
||||
: placeholder}
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<PopoverContent className="w-100 p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={placeholder} className="h-9" />
|
||||
<CommandList>
|
||||
@@ -60,7 +66,8 @@ export function ComboBox({
|
||||
key={ent.value}
|
||||
value={ent.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue);
|
||||
if (onValueSelect) onValueSelect(currentValue);
|
||||
else setValue(currentValue === value ? "" : currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
+2
-2
@@ -188,7 +188,7 @@ export async function tlState(
|
||||
or(
|
||||
eq(tierlistStates.uuid, `${data.uuid}`),
|
||||
and(
|
||||
eq(tierlistStates.char, `${data.char}`),
|
||||
eq(tierlistStates.ref, `${data.ref}`),
|
||||
eq(tierlistStates.list, `${data.list}`),
|
||||
),
|
||||
),
|
||||
@@ -197,7 +197,7 @@ export async function tlState(
|
||||
await db
|
||||
.update(tierlistStates)
|
||||
.set(data)
|
||||
.where(eq(tierlistStates.uuid, `${data.uuid}`));
|
||||
.where(eq(tierlistStates.uuid, existing.uuid));
|
||||
else
|
||||
await db
|
||||
.insert(tierlistStates)
|
||||
|
||||
@@ -15,6 +15,7 @@ type SharedStates = Partial<{
|
||||
connected: boolean;
|
||||
updated: boolean;
|
||||
"rubgram.services": string[];
|
||||
"tl.deleteMode": boolean;
|
||||
_raw: Record<string, unknown>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -305,6 +305,7 @@ export const tierlistStates = tierlist.table("states", {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
ref: text().notNull(), // for manually created tiles, the id would be char#Date.now() instead
|
||||
list: text()
|
||||
.notNull()
|
||||
.references(() => tierlistVersions.id, {
|
||||
|
||||
Reference in New Issue
Block a user