TierList visuals complete
Co-authored-by: Gunshin <[email protected]> Co-authored-by: S4mSh1ne <[email protected]>
This commit is contained in:
co-authored by
Gunshin
S4mSh1ne
Unverified
parent
ca652d273d
commit
b66212acf8
@@ -40,9 +40,6 @@ RUN bun run build
|
||||
### Stage 3: runner ###
|
||||
FROM oven/bun:latest AS runner
|
||||
|
||||
# Install jc (disk usage info)
|
||||
RUN apt update && apt install jc -y
|
||||
|
||||
# Isolation
|
||||
RUN useradd -mu 1001 container
|
||||
USER container
|
||||
|
||||
@@ -15,7 +15,7 @@ export function EnkaBrowser({ uid }: { uid: string }) {
|
||||
return (
|
||||
<div className="h-full w-full relative">
|
||||
{!ready && (
|
||||
<Loader2 className="animate-spin absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-8 text-muted-foreground z-100" />
|
||||
<Loader2 className="animate-spin absolute top-[calc(50%+5rem] left-1/2 -translate-x-1/2 -translate-y-1/2 size-8 text-muted-foreground z-100" />
|
||||
)}
|
||||
<iframe
|
||||
src={`https://enka.network/u/${id}`}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useProgress } from "@bprogress/next";
|
||||
import { Lock, Unlock } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
@@ -43,8 +44,14 @@ export function SidebarLink({
|
||||
submission: { id: string; name: string; checked: boolean; queue: number };
|
||||
className?: string;
|
||||
}) {
|
||||
const { stop } = useProgress();
|
||||
const { id } = useParams();
|
||||
const router = useRouter();
|
||||
const [checked, setChecked] = useState(submission.checked);
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(submission.checked);
|
||||
}, [submission.checked]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/artifact/admin/${submission.id}`}
|
||||
@@ -52,15 +59,22 @@ export function SidebarLink({
|
||||
className,
|
||||
id === submission.id && "bg-accent text-accent-foreground",
|
||||
)}
|
||||
prefetch
|
||||
onClick={(ev) => {
|
||||
if ((ev.target as HTMLButtonElement).type === "button") {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{submission.queue}. {submission.name}
|
||||
<SidebarMenuBadge className="pointer-events-auto">
|
||||
<Checkbox
|
||||
className="mr-2"
|
||||
checked={submission.checked}
|
||||
checked={checked}
|
||||
onCheckedChange={async () => {
|
||||
setChecked((x) => !x);
|
||||
await toggleCheck(submission.id);
|
||||
router.refresh();
|
||||
stop();
|
||||
}}
|
||||
/>
|
||||
</SidebarMenuBadge>
|
||||
@@ -199,4 +213,4 @@ export function Watcher() {
|
||||
}, [router]);
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Dice3, Trash2 } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
SidebarProvider,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { getArtifactConfig, random, wipe } from "@/lib/api";
|
||||
import { apiAuthCheck } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { submissions } from "@/lib/db/schema";
|
||||
import { LimitManager, SubmissionList, Watcher } from "./client";
|
||||
@@ -32,6 +34,7 @@ export default async function AdminLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
if (!(await apiAuthCheck())) redirect("/");
|
||||
const subs = await db
|
||||
.select({
|
||||
id: submissions.id,
|
||||
@@ -63,7 +66,7 @@ export default async function AdminLayout({
|
||||
<Trash2 size={24} className="size-6 text-red-500" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent className="z-101">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you absolutely sure?
|
||||
|
||||
+22
-24
@@ -49,15 +49,36 @@ export default async function ArtifactFormPage() {
|
||||
return (
|
||||
<div className="flex flex-col justify-around items-center h-full">
|
||||
<Card className="w-full max-w-md">
|
||||
{q ? (
|
||||
<Blocker>
|
||||
<div className="flex gap-1 flex-col items-center">
|
||||
<span className="font-bold text-3xl">คิวของคุณคือหมายเลข</span>
|
||||
<span className="font-bold text-5xl">{q.queue}</span>
|
||||
</div>
|
||||
</Blocker>
|
||||
) : config.locked ? (
|
||||
<Blocker>
|
||||
<span className="font-bold text-3xl">ยังไม่เปิดรับคิว</span>
|
||||
</Blocker>
|
||||
) : config.limit >= 0 && count >= config.limit ? (
|
||||
<Blocker>
|
||||
<div className="flex gap-1 flex-col items-center">
|
||||
<span className="font-bold text-3xl">คิวเต็มแล้ว</span>
|
||||
<span className="font-bold text-2xl">ต้องโดเนทลัดคิวแล้วล่ะ</span>
|
||||
</div>
|
||||
</Blocker>
|
||||
) : (
|
||||
<Disclaimer />
|
||||
)}
|
||||
<CardHeader className="justify-center">
|
||||
<CardTitle>
|
||||
<div className="w-[276.5px]">
|
||||
<Link href="/">
|
||||
<Image
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: "translateY(-70%)",
|
||||
}}
|
||||
className="absolute z-50"
|
||||
height={137.5}
|
||||
width={276.5}
|
||||
src={banner}
|
||||
@@ -70,29 +91,6 @@ export default async function ArtifactFormPage() {
|
||||
<CardContent>
|
||||
<ArtifactFormWrapper id="mainform">
|
||||
<div className="flex flex-col gap-3">
|
||||
{q ? (
|
||||
<Blocker>
|
||||
<div className="flex gap-1 flex-col items-center">
|
||||
<span className="font-bold text-3xl">คิวของคุณคือหมายเลข</span>
|
||||
<span className="font-bold text-5xl">{q.queue}</span>
|
||||
</div>
|
||||
</Blocker>
|
||||
) : config.locked ? (
|
||||
<Blocker>
|
||||
<span className="font-bold text-3xl">ยังไม่เปิดรับคิว</span>
|
||||
</Blocker>
|
||||
) : config.limit >= 0 && count >= config.limit ? (
|
||||
<Blocker>
|
||||
<div className="flex gap-1 flex-col items-center">
|
||||
<span className="font-bold text-3xl">คิวเต็มแล้ว</span>
|
||||
<span className="font-bold text-2xl">
|
||||
ต้องโดเนทลัดคิวแล้วล่ะ
|
||||
</span>
|
||||
</div>
|
||||
</Blocker>
|
||||
) : (
|
||||
<Disclaimer />
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">ชื่อ*</Label>
|
||||
<Input
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -137,3 +137,8 @@ input[type="number"] {
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-blue-500) var(--card);
|
||||
}
|
||||
|
||||
+4
-5
@@ -20,7 +20,7 @@ export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://buzz.gunshiz.top"),
|
||||
title: "เกนชินไม่ใช่เกมมือถือ",
|
||||
description: "ระบบอีเวนท์ของเกนชินไม่ใช่เกมมือถือ",
|
||||
icons: "https://buzz.gunshiz.top/favicon.webp",
|
||||
icons: "https://buzzy.gunshiz.top/favicon.webp", // TODO: CHANGE BACK TO buzz
|
||||
openGraph: {
|
||||
title: "เกนชินไม่ใช่เกมมือถือ",
|
||||
description: "ระบบอีเวนท์ของเกนชินไม่ใช่เกมมือถือ",
|
||||
@@ -37,15 +37,14 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="h-full">
|
||||
<html lang="en" className="h-full dark">
|
||||
<body
|
||||
className={`${anuphan.variable} ${geistMono.variable} font-sans antialiased dark h-full`}
|
||||
className={`${anuphan.variable} ${geistMono.variable} font-sans antialiased h-full`}
|
||||
>
|
||||
<Image
|
||||
src={Background}
|
||||
alt="Background"
|
||||
fill
|
||||
className="z-[-1] opacity-20 object-cover"
|
||||
className="z-[-1] opacity-20 object-cover fixed top-0 left-0 w-full h-full"
|
||||
/>
|
||||
<Providers>{children}</Providers>
|
||||
<Toaster />
|
||||
|
||||
+28
-10
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { UserLock } from "lucide-react";
|
||||
import { Loader2, UserLock } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type FormEvent, use } from "react";
|
||||
import { type FormEvent, use, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -14,22 +14,40 @@ export default function LoginPage({
|
||||
}: {
|
||||
searchParams: Promise<{ next?: string }>;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const { next } = use(searchParams);
|
||||
const cb = next || "/admin";
|
||||
|
||||
useEffect(() => {
|
||||
async function checkSession() {
|
||||
const { data } = await authClient.getSession();
|
||||
if (data) router.push(cb);
|
||||
}
|
||||
checkSession();
|
||||
}, [cb, router]);
|
||||
|
||||
async function submit(ev: FormEvent<HTMLFormElement>) {
|
||||
ev.preventDefault();
|
||||
setLoading(true);
|
||||
const data = new FormData(ev.currentTarget);
|
||||
const r = await authClient.signUp
|
||||
const r = await authClient.signIn
|
||||
.email({
|
||||
email: data.get("email") as string,
|
||||
password: data.get("password") as string,
|
||||
name: data.get("name") as string,
|
||||
// name: data.get("name") as string,
|
||||
})
|
||||
.catch(() => toast.error("Failed to login"));
|
||||
if (typeof r !== "object") return toast.error("Invalid email or password");
|
||||
if (r.error) return toast.error(r.error.message);
|
||||
if (typeof r !== "object") {
|
||||
setLoading(false);
|
||||
return toast.error("Invalid email or password");
|
||||
}
|
||||
if (r.error) {
|
||||
setLoading(false);
|
||||
return toast.error(r.error.message);
|
||||
}
|
||||
router.push(cb);
|
||||
setLoading(false);
|
||||
}
|
||||
return (
|
||||
<div className="bg-[#1117] backdrop-blur-lg flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
@@ -59,7 +77,7 @@ export default function LoginPage({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="grid gap-1">
|
||||
{/* <div className="grid gap-1">
|
||||
<Label htmlFor="email">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
@@ -69,7 +87,7 @@ export default function LoginPage({
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
@@ -90,8 +108,8 @@ export default function LoginPage({
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? <Loader2 className="animate-spin" /> : "Login"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,9 @@ export default function Home() {
|
||||
<Link href="/artifact">
|
||||
<Button>Artifact</Button>
|
||||
</Link>
|
||||
<Link href="/tl">
|
||||
<Button>TierList</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import { type ReactNode, useContext } from "react";
|
||||
import type { tierlistColumns, tierlistTiers } from "@/lib/db/schema";
|
||||
import { TierListContext } from "./context";
|
||||
|
||||
export function TierListCell({
|
||||
tier,
|
||||
column,
|
||||
children,
|
||||
}: {
|
||||
tier: typeof tierlistTiers.$inferSelect;
|
||||
column: typeof tierlistColumns.$inferSelect;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const { tileSize } = useContext(TierListContext);
|
||||
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: `${tier.id}-${column.id}`,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className="flex flex-wrap p-1 gap-2 items-start"
|
||||
style={{
|
||||
minHeight: `${tileSize + 8}px`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
import Image from "next/image";
|
||||
import { useContext } from "react";
|
||||
import type { characters } from "@/lib/db/schema";
|
||||
import { TierListContext } from "./context";
|
||||
|
||||
export function Draggable({ char }: { char: typeof characters.$inferSelect }) {
|
||||
const { tileSize } = useContext(TierListContext);
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: char.id,
|
||||
});
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
<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}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
import { createContext } from "react";
|
||||
import type { characters, tierlistBadges } from "@/lib/db/schema";
|
||||
|
||||
export const TierListContext = createContext<{
|
||||
chars: (typeof characters.$inferSelect)[];
|
||||
badges: (typeof tierlistBadges.$inferSelect)[];
|
||||
tileSize: number;
|
||||
}>({
|
||||
chars: [],
|
||||
badges: [],
|
||||
tileSize: 64,
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
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 badges = await tx
|
||||
.select()
|
||||
.from(tierlistBadges)
|
||||
.orderBy(tierlistBadges.order);
|
||||
|
||||
return { type: typeInfo, version, tiers, columns, badges, chars };
|
||||
});
|
||||
// console.log(config);
|
||||
if (!config) notFound();
|
||||
return <TierList {...config} />;
|
||||
// return <pre>{JSON.stringify(config, null, 2)}</pre>;
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
DragOverlay,
|
||||
useDroppable,
|
||||
} from "@dnd-kit/core";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Fragment,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import Favicon from "#/favicon.webp";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type {
|
||||
characters,
|
||||
tierlistBadges,
|
||||
tierlistColumns,
|
||||
tierlistTiers,
|
||||
tierlistTypes,
|
||||
tierlistVersions,
|
||||
} from "@/lib/db/schema";
|
||||
import { TierListCell } from "./cell";
|
||||
import { Draggable } from "./character";
|
||||
import { TierListContext } from "./context";
|
||||
|
||||
function Untiered({
|
||||
children,
|
||||
ref,
|
||||
open
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
ref?: RefObject<HTMLDivElement | null>;
|
||||
open?: boolean;
|
||||
}) {
|
||||
const { tileSize } = useContext(TierListContext);
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: "untiered",
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`flex flex-wrap ${open?"p-1":""} gap-2 bg-[#0005] overflow-auto`}
|
||||
style={{
|
||||
maxHeight: `${(tileSize + 4) * 3 + 4}px`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TierList({
|
||||
type,
|
||||
version,
|
||||
chars,
|
||||
tiers,
|
||||
columns,
|
||||
badges,
|
||||
}: {
|
||||
type: typeof tierlistTypes.$inferSelect;
|
||||
version: typeof tierlistVersions.$inferSelect;
|
||||
chars: (typeof characters.$inferSelect)[];
|
||||
tiers: (typeof tierlistTiers.$inferSelect)[];
|
||||
columns: (typeof tierlistColumns.$inferSelect)[];
|
||||
badges: (typeof tierlistBadges.$inferSelect)[];
|
||||
}) {
|
||||
const [tileSizeSetting, setTileSize] = useLocalStorage<number | null>(
|
||||
"tl_tileSize",
|
||||
null,
|
||||
);
|
||||
const [tileSizeAuto, setTileSizeAuto] = useState(0);
|
||||
const tileSize = tileSizeSetting || tileSizeAuto;
|
||||
|
||||
const colRef = useRef<HTMLDivElement | null>(null);
|
||||
const untieredRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
//#region tileSize calculation
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: falsy
|
||||
useLayoutEffect(() => {
|
||||
function recalc() {
|
||||
let auto = 0;
|
||||
let tiles = Math.ceil(24 / columns.length);
|
||||
do {
|
||||
if (!colRef.current || !untieredRef.current) return;
|
||||
|
||||
const colRect = colRef.current.getBoundingClientRect();
|
||||
const untieredRect = untieredRef.current.getBoundingClientRect();
|
||||
|
||||
const colPaddingLeft = 4; // or measure with getComputedStyle(colRef.current)
|
||||
const contPaddingLeft = 4;
|
||||
const tilePaddingRight = 8;
|
||||
|
||||
// tiered (always 6 cols)
|
||||
const tileSizeCol =
|
||||
(colRect.width - colPaddingLeft) / tiles - tilePaddingRight;
|
||||
|
||||
// untiered → calculate how many columns fit with ~tileSizeCol
|
||||
let x = Math.floor(
|
||||
(untieredRect.width - contPaddingLeft) /
|
||||
(tileSizeCol + tilePaddingRight),
|
||||
);
|
||||
if (x < 1) x = 1;
|
||||
|
||||
const tileSizeUntiered =
|
||||
(untieredRect.width - contPaddingLeft) / x - tilePaddingRight;
|
||||
|
||||
console.log(tileSizeCol, tileSizeUntiered);
|
||||
// final auto size
|
||||
auto = Math.min(tileSizeCol, tileSizeUntiered);
|
||||
tiles--;
|
||||
} while (auto < 50 && tiles > 0);
|
||||
setTileSizeAuto(auto);
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(recalc);
|
||||
if (colRef.current) observer.observe(colRef.current);
|
||||
if (untieredRef.current) observer.observe(untieredRef.current);
|
||||
|
||||
// run once immediately
|
||||
recalc();
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [columns.length, colRef.current, untieredRef.current]);
|
||||
//#endregion tileSize
|
||||
|
||||
const [untieredOpen, setUntieredOpen] = useLocalStorage(
|
||||
"tl_untieredOpen",
|
||||
true,
|
||||
);
|
||||
|
||||
//#region placements handling
|
||||
const initialPlacements: Record<string, string[]> = {
|
||||
untiered: chars.map((ch) => ch.id),
|
||||
...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,
|
||||
);
|
||||
const [dragging, setDragging] = useState<string | null>(null);
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
if (!over) return;
|
||||
const charId = active.id as string;
|
||||
const target = over.id as string;
|
||||
let source: string | undefined;
|
||||
for (const cell in placements) {
|
||||
if (placements[cell].includes(charId)) {
|
||||
source = cell;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!source || source === target) return;
|
||||
setPlacements({
|
||||
...placements,
|
||||
[source]: placements[source].filter((id) => id !== charId),
|
||||
[target]: [...placements[target], charId],
|
||||
});
|
||||
}
|
||||
//#endregion placements
|
||||
|
||||
return (
|
||||
<TierListContext.Provider value={{ badges, chars, tileSize }}>
|
||||
<DndContext
|
||||
onDragStart={(e) => setDragging(e.active.id as string)}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<div className="flex-1 min-h-0 overflow-auto">
|
||||
<div
|
||||
className={`grid w-full [&>*]:border`}
|
||||
style={{
|
||||
gridTemplateColumns: `min-content repeat(${columns.length}, 1fr)`,
|
||||
gridTemplateRows: `min-content repeat(${tiers.length}, minmax(0, min-content))`,
|
||||
}}
|
||||
>
|
||||
<Dialog>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex flex-col font-bold items-center justify-center aspect-square w-min h-min whitespace-nowrap bg-[#2225] cursor-pointer">
|
||||
<span>{type.name}</span>
|
||||
<span>{version.name}</span>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<Link href="/tl">
|
||||
<DropdownMenuItem>หน้าหลัก</DropdownMenuItem>
|
||||
</Link>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem>การตั้งค่า</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>การตั้งค่า</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-between">
|
||||
<div className="grid gap-2 group">
|
||||
<span>
|
||||
ขนาดตัวละคร{" "}
|
||||
<span className="text-muted-foreground group-hover:opacity-100 opacity-0 transition-opacity">
|
||||
(px)
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={tileSize}
|
||||
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>
|
||||
<Image
|
||||
src={Favicon}
|
||||
alt="Test Image"
|
||||
width={tileSize}
|
||||
height={tileSize}
|
||||
className="rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{columns.map((c) => (
|
||||
<div
|
||||
key={`C${c.id}`}
|
||||
className="bg-[#0005] text-2xl font-bold flex items-center justify-center relative"
|
||||
ref={colRef}
|
||||
>
|
||||
{c.image ? (
|
||||
<Image
|
||||
src={`/cdn/${c.image}`}
|
||||
alt={c.name}
|
||||
fill
|
||||
className="p-2 object-cover"
|
||||
/>
|
||||
) : (
|
||||
c.name
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{tiers.map((t) => (
|
||||
<Fragment key={`T${t.id}`}>
|
||||
<div className="bg-[#0005] text-4xl font-bold flex items-center justify-center">
|
||||
{t.image ? (
|
||||
<Image
|
||||
src={`/cdn/${t.image}`}
|
||||
alt={t.name}
|
||||
height={100}
|
||||
width={100}
|
||||
/>
|
||||
) : (
|
||||
t.name
|
||||
)}
|
||||
</div>
|
||||
{columns.map((c) => {
|
||||
const cellId = `${t.id}-${c.id}`;
|
||||
const cellChars = placements[cellId].map(
|
||||
(id) => chars.find((ch) => ch.id === id)!,
|
||||
);
|
||||
return (
|
||||
<TierListCell key={cellId} column={c} tier={t}>
|
||||
{cellChars.map((ch) => (
|
||||
<Draggable key={ch.id} char={ch} />
|
||||
))}
|
||||
</TierListCell>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
onClick={() => setUntieredOpen(!untieredOpen)}
|
||||
variant="outline"
|
||||
className="rounded-none"
|
||||
>
|
||||
({placements.untiered.length}) ตัวละครที่ยังไม่ได้จัดเทียร์
|
||||
{untieredOpen ? (
|
||||
<ChevronDown className="ml-1" />
|
||||
) : (
|
||||
<ChevronUp className="ml-1" />
|
||||
)}
|
||||
</Button>
|
||||
<Untiered ref={untieredRef} open={untieredOpen}>
|
||||
{untieredOpen &&
|
||||
placements.untiered.map((id) => {
|
||||
const ch = chars.find((c) => c.id === id)!;
|
||||
return <Draggable key={ch.id} char={ch} />;
|
||||
})}
|
||||
</Untiered>
|
||||
</div>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
{dragging && (
|
||||
<Draggable char={chars.find((c) => c.id === dragging)!} />
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</TierListContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { db } from "@/lib/db";
|
||||
import { tierlistTypes, tierlistVersions } from "@/lib/db/schema";
|
||||
|
||||
export default async function TierlistSelectionPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ type: string }>;
|
||||
}) {
|
||||
const { type: typeName } = await params;
|
||||
const [type] = await db
|
||||
.select()
|
||||
.from(tierlistTypes)
|
||||
.where(and(eq(tierlistTypes.id, typeName)));
|
||||
if (!type) notFound();
|
||||
const versions = await db
|
||||
.select()
|
||||
.from(tierlistVersions)
|
||||
.where(
|
||||
and(
|
||||
eq(tierlistVersions.hidden, false),
|
||||
eq(tierlistVersions.type, typeName),
|
||||
),
|
||||
);
|
||||
return (
|
||||
<div className="w-full min-h-full flex justify-center">
|
||||
<div className="min-h-full flex flex-col items-center justify-center gap-2">
|
||||
<div className="grid gap-1">
|
||||
<center className="font-bold text-4xl">{type.name}</center>
|
||||
<div className="flex gap-2 flex-wrap justify-center">
|
||||
{[
|
||||
...versions.map((e) => ({ ...e, id: `${e.id}1` })),
|
||||
...versions.map((e) => ({ ...e, id: `${e.id}2` })),
|
||||
...versions.map((e) => ({ ...e, id: `${e.id}3` })),
|
||||
...versions.map((e) => ({ ...e, id: `${e.id}4` })),
|
||||
...versions.map((e) => ({ ...e, id: `${e.id}6` })),
|
||||
].map((e) =>
|
||||
e.image ? (
|
||||
<Link href={`/tl/${type.id}/${e.id}`} key={e.id}>
|
||||
<div className="relative aspect-video w-80 rounded-sm border">
|
||||
<Image
|
||||
src={`/cdn/${e.image}`}
|
||||
alt={e.name}
|
||||
fill
|
||||
className="rounded-sm border object-cover bg-primary"
|
||||
/>
|
||||
<div className="flex absolute w-full justify-center bottom-0 py-1 bg-card rounded-b-sm">
|
||||
{e.name}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/tl/${type.id}/${e.id}`} key={e.id}>
|
||||
<div className="flex items-center justify-center aspect-video w-80 border rounded-sm bg-[#1118] backdrop-blur-xl font-bold text-4xl">
|
||||
{e.name}
|
||||
</div>
|
||||
</Link>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { db } from "@/lib/db";
|
||||
import { tierlistTypes, tierlistVersions } from "@/lib/db/schema";
|
||||
|
||||
export default async function TierlistSelectionPage() {
|
||||
const types = await db.select().from(tierlistTypes).orderBy(tierlistTypes.id);
|
||||
const versionsList = await db
|
||||
.select()
|
||||
.from(tierlistVersions)
|
||||
.orderBy(desc(tierlistVersions.id));
|
||||
const vers = types.map((t) => ({
|
||||
...t,
|
||||
versions: versionsList.filter((v) => v.type === t.id),
|
||||
}));
|
||||
return (
|
||||
<div className="max-w-full min-h-full flex flex-col justify-center gap-2 mx-2">
|
||||
<center className="text-6xl font-bold mb-8">
|
||||
<Link href="/">จัดเทียร์ลิสต์</Link>
|
||||
</center>
|
||||
{vers.map((t) => (
|
||||
<div className="flex flex-col gap-1" key={t.id}>
|
||||
<div className="font-bold text-4xl">
|
||||
<div className="px-2 py-1 border border-dashed w-min rounded-md bg-[#2228]">
|
||||
{t.name}
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea>
|
||||
<div className="flex gap-2 max-w-full">
|
||||
{t.versions.map((e) => (
|
||||
<Link href={`/tl/${t.id}/${e.id}`} key={e.id}>
|
||||
{e.image ? (
|
||||
<div className="relative aspect-video w-60 rounded-sm border">
|
||||
<Image
|
||||
src={`/cdn/${e.image}`}
|
||||
alt={e.name}
|
||||
fill
|
||||
className="rounded-sm border object-cover bg-primary"
|
||||
/>
|
||||
<div className="flex absolute w-full justify-center bottom-0 py-1 bg-card rounded-b-sm">
|
||||
{e.name}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center aspect-video w-60 border rounded-sm bg-[#1118] backdrop-blur-xl font-bold text-4xl">
|
||||
{e.name}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"name": "buzzy",
|
||||
"dependencies": {
|
||||
"@bprogress/next": "^3.2.12",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@headless-tree/core": "^1.4.0",
|
||||
"@headless-tree/react": "^1.4.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
@@ -44,7 +45,7 @@
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "15.5.3",
|
||||
"next": "^15.5.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"pg": "^8.16.3",
|
||||
"radix-ui": "^1.4.3",
|
||||
@@ -56,6 +57,7 @@
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"uuidv7": "^1.0.2",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.1.11",
|
||||
@@ -197,6 +199,12 @@
|
||||
|
||||
"@date-fns/tz": ["@date-fns/[email protected]", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
||||
|
||||
"@dnd-kit/accessibility": ["@dnd-kit/[email protected]", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
|
||||
|
||||
"@dnd-kit/core": ["@dnd-kit/[email protected]", "", { "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/utilities": ["@dnd-kit/[email protected]", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/[email protected]", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||
@@ -335,23 +343,23 @@
|
||||
|
||||
"@mrleebo/prisma-ast": ["@mrleebo/[email protected]", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-VxS+okLpNp6YUDLz3rAXv6sE8G4W5cX039bumkPub65xRr9BFxjHaT5ebX4DsiixIGaBwitjWTvx3mAbyI2EUQ=="],
|
||||
|
||||
"@next/env": ["@next/[email protected].3", "", {}, "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw=="],
|
||||
"@next/env": ["@next/[email protected].4", "", {}, "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/[email protected].3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg=="],
|
||||
"@next/swc-darwin-arm64": ["@next/[email protected].4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/[email protected].3", "", { "os": "darwin", "cpu": "x64" }, "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g=="],
|
||||
"@next/swc-darwin-x64": ["@next/[email protected].4", "", { "os": "darwin", "cpu": "x64" }, "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/[email protected].3", "", { "os": "linux", "cpu": "arm64" }, "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw=="],
|
||||
"@next/swc-linux-arm64-gnu": ["@next/[email protected].4", "", { "os": "linux", "cpu": "arm64" }, "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/[email protected].3", "", { "os": "linux", "cpu": "arm64" }, "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ=="],
|
||||
"@next/swc-linux-arm64-musl": ["@next/[email protected].4", "", { "os": "linux", "cpu": "arm64" }, "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/[email protected].3", "", { "os": "linux", "cpu": "x64" }, "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA=="],
|
||||
"@next/swc-linux-x64-gnu": ["@next/[email protected].4", "", { "os": "linux", "cpu": "x64" }, "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/[email protected].3", "", { "os": "linux", "cpu": "x64" }, "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg=="],
|
||||
"@next/swc-linux-x64-musl": ["@next/[email protected].4", "", { "os": "linux", "cpu": "x64" }, "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/[email protected].3", "", { "os": "win32", "cpu": "arm64" }, "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA=="],
|
||||
"@next/swc-win32-arm64-msvc": ["@next/[email protected].4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/[email protected].3", "", { "os": "win32", "cpu": "x64" }, "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw=="],
|
||||
"@next/swc-win32-x64-msvc": ["@next/[email protected].4", "", { "os": "win32", "cpu": "x64" }, "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/[email protected]", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="],
|
||||
|
||||
@@ -767,6 +775,8 @@
|
||||
|
||||
"lodash": ["[email protected]", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"lodash.debounce": ["[email protected]", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
||||
|
||||
"loose-envify": ["[email protected]", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
@@ -785,7 +795,7 @@
|
||||
|
||||
"nanostores": ["[email protected]", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="],
|
||||
|
||||
"next": ["[email protected].3", "", { "dependencies": { "@next/env": "15.5.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.3", "@next/swc-darwin-x64": "15.5.3", "@next/swc-linux-arm64-gnu": "15.5.3", "@next/swc-linux-arm64-musl": "15.5.3", "@next/swc-linux-x64-gnu": "15.5.3", "@next/swc-linux-x64-musl": "15.5.3", "@next/swc-win32-arm64-msvc": "15.5.3", "@next/swc-win32-x64-msvc": "15.5.3", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw=="],
|
||||
"next": ["[email protected].4", "", { "dependencies": { "@next/env": "15.5.4", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.4", "@next/swc-darwin-x64": "15.5.4", "@next/swc-linux-arm64-gnu": "15.5.4", "@next/swc-linux-arm64-musl": "15.5.4", "@next/swc-linux-x64-gnu": "15.5.4", "@next/swc-linux-x64-musl": "15.5.4", "@next/swc-win32-arm64-msvc": "15.5.4", "@next/swc-win32-x64-msvc": "15.5.4", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA=="],
|
||||
|
||||
"next-themes": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
||||
@@ -943,6 +953,8 @@
|
||||
|
||||
"use-sync-external-store": ["[email protected]", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||
|
||||
"usehooks-ts": ["[email protected]", "", { "dependencies": { "lodash.debounce": "^4.0.8" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA=="],
|
||||
|
||||
"uuidv7": ["[email protected]", "", { "bin": { "uuidv7": "cli.js" } }, "sha512-8JQkH4ooXnm1JCIhqTMbtmdnYEn6oKukBxHn1Ic9878jMkL7daTI7anTExfY18VRCX7tcdn5quzvCb6EWrR8PA=="],
|
||||
|
||||
"vaul": ["[email protected]", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Blocker({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"blocker backdrop-blur-sm bg-[#2225] flex justify-center items-center absolute top-[-5px] left-[-5px] bottom-[-5px] right-[-5px] rounded-sm z-100 border-t-gray-700 border-l-gray-600 border-1",
|
||||
"blocker backdrop-blur-sm bg-[#2225] flex justify-center items-center absolute top-[-5px] left-[-5px] bottom-[-5px] right-[-5px] rounded-sm z-45 border-t-gray-700 border-l-gray-600 border-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import type * as React from "react";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
@@ -17,7 +16,7 @@ function AlertDialogTrigger({
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
@@ -25,7 +24,7 @@ function AlertDialogPortal({
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
@@ -37,11 +36,11 @@ function AlertDialogOverlay({
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
@@ -55,12 +54,12 @@ function AlertDialogContent({
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
@@ -73,7 +72,7 @@ function AlertDialogHeader({
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
@@ -85,11 +84,11 @@ function AlertDialogFooter({
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
@@ -102,7 +101,7 @@ function AlertDialogTitle({
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
@@ -115,7 +114,7 @@ function AlertDialogDescription({
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
@@ -127,7 +126,7 @@ function AlertDialogAction({
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
@@ -139,7 +138,7 @@ function AlertDialogCancel({
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -154,4 +153,4 @@ export {
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
};
|
||||
|
||||
+45
-2
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { eq, inArray, not, sql } from "drizzle-orm";
|
||||
import type { PgColumn, PgTable } from "drizzle-orm/pg-core";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { apiAuthCheck } from "./auth";
|
||||
@@ -77,6 +78,7 @@ export async function toggleCheck(submissionId: string) {
|
||||
})
|
||||
.where(eq(submissions.id, submissionId));
|
||||
revalidatePath("/artifact/admin");
|
||||
revalidatePath("/artifact");
|
||||
}
|
||||
|
||||
export async function toggleLock() {
|
||||
@@ -93,7 +95,7 @@ export async function toggleLock() {
|
||||
)
|
||||
await db.insert(artifactSettings).values({ locked: true });
|
||||
revalidatePath("/artifact/admin");
|
||||
revalidatePath("/");
|
||||
revalidatePath("/artifact");
|
||||
}
|
||||
|
||||
export async function setLimit(limit: number) {
|
||||
@@ -111,7 +113,7 @@ export async function setLimit(limit: number) {
|
||||
)
|
||||
await db.insert(artifactSettings).values({ limit });
|
||||
revalidatePath("/artifact/admin");
|
||||
revalidatePath("/");
|
||||
revalidatePath("/artifact");
|
||||
}
|
||||
|
||||
export async function wipe() {
|
||||
@@ -121,6 +123,7 @@ export async function wipe() {
|
||||
sql`ALTER SEQUENCE artifact.submissions_queue_seq RESTART WITH 1`,
|
||||
);
|
||||
revalidatePath("/artifact/admin");
|
||||
revalidatePath("/artifact");
|
||||
redirect("/artifact/admin");
|
||||
}
|
||||
|
||||
@@ -135,3 +138,43 @@ export async function random() {
|
||||
if (sub) redirect(`/artifact/admin/${sub.id}`);
|
||||
else throw "ไม่พบผู้ลงทะเบียนที่ยังไม่ตรวจสอบ";
|
||||
}
|
||||
|
||||
export async function reorder(
|
||||
table: PgTable & { id: PgColumn; order: PgColumn },
|
||||
id: string,
|
||||
position: number,
|
||||
) {
|
||||
await db
|
||||
.update(table)
|
||||
.set({
|
||||
order: sql`
|
||||
(
|
||||
SELECT (prev_order + next_order) / 2
|
||||
FROM (
|
||||
SELECT "order",
|
||||
lag("order") OVER (ORDER BY "order") AS prev_order,
|
||||
lead("order") OVER (ORDER BY "order") AS next_order,
|
||||
row_number() OVER (ORDER BY "order") AS rn
|
||||
FROM ${table}
|
||||
) sub
|
||||
WHERE rn = ${position}
|
||||
)
|
||||
`,
|
||||
})
|
||||
.where(eq(table.id, id));
|
||||
}
|
||||
|
||||
export async function untangle(
|
||||
table: PgTable & { id: PgColumn; order: PgColumn },
|
||||
) {
|
||||
await db.execute(sql`
|
||||
WITH ordered AS (
|
||||
SELECT id, row_number() OVER (ORDER BY "order") * 10 AS new_order
|
||||
FROM ${table}
|
||||
)
|
||||
UPDATE ${table} t
|
||||
SET "order" = o.new_order
|
||||
FROM ordered o
|
||||
WHERE t.id = o.id;
|
||||
`);
|
||||
}
|
||||
|
||||
+8
-3
@@ -7,9 +7,14 @@ import * as schema from "$/db/schema";
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
// disableSignUp: true,
|
||||
disableSignUp: true,
|
||||
},
|
||||
trustedOrigins: ["http://nyx:3000"],
|
||||
trustedOrigins: [
|
||||
"http://nyx:3000",
|
||||
"http://astral:3000",
|
||||
"http://localhost:3000",
|
||||
"https://dev3000.dgnr.us",
|
||||
],
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg", // or "mysql", "sqlite"
|
||||
schema,
|
||||
@@ -23,7 +28,7 @@ export async function apiAuthCheck() {
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
console.log(session);
|
||||
// console.log(session);
|
||||
|
||||
return session?.user;
|
||||
}
|
||||
|
||||
+23
-14
@@ -1,14 +1,15 @@
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
date,
|
||||
integer,
|
||||
pgEnum,
|
||||
pgSchema,
|
||||
pgTable,
|
||||
serial,
|
||||
text,
|
||||
timestamp,
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
date,
|
||||
integer,
|
||||
numeric,
|
||||
pgEnum,
|
||||
pgSchema,
|
||||
pgTable,
|
||||
serial,
|
||||
text,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { uuidv7 } from "uuidv7";
|
||||
import { bytea } from "./custom";
|
||||
@@ -27,6 +28,7 @@ export const element = pgEnum("character_element", [
|
||||
export const cdn = pgTable("cdn", {
|
||||
id: text().primaryKey().$defaultFn(uuidv7),
|
||||
data: bytea().notNull(),
|
||||
name: text(),
|
||||
type: text().notNull(),
|
||||
size: text().notNull(),
|
||||
});
|
||||
@@ -50,6 +52,7 @@ export const characters = pgTable("characters", {
|
||||
.references(() => cdn.id),
|
||||
weapon: text().notNull(),
|
||||
amber: text().notNull(), // Amber character ID
|
||||
order: numeric().notNull(),
|
||||
});
|
||||
|
||||
export const versions = pgTable("versions", {
|
||||
@@ -95,27 +98,30 @@ export const tierlist = pgSchema("tierlist");
|
||||
export const tierlistTypes = tierlist.table("types", {
|
||||
id: text().primaryKey().$defaultFn(uuidv7),
|
||||
name: text().notNull(),
|
||||
image: text()
|
||||
.notNull()
|
||||
.references(() => cdn.id),
|
||||
image: text().references(() => cdn.id),
|
||||
order: numeric().notNull(),
|
||||
});
|
||||
|
||||
export const tierlistTiers = tierlist.table("tiers", {
|
||||
id: text().primaryKey().$defaultFn(uuidv7),
|
||||
name: text().notNull(),
|
||||
badges: text().references(()=>tierlistBadges.id).array(),
|
||||
image: text().references(() => cdn.id),
|
||||
order: numeric().notNull(),
|
||||
});
|
||||
|
||||
export const tierlistColumns = tierlist.table("columns", {
|
||||
id: text().primaryKey().$defaultFn(uuidv7),
|
||||
name: text().notNull(),
|
||||
image: text().references(() => cdn.id),
|
||||
order: numeric().notNull(),
|
||||
});
|
||||
|
||||
export const tierlistBadges = tierlist.table("badges", {
|
||||
id: text().primaryKey().$defaultFn(uuidv7),
|
||||
name: text().notNull(),
|
||||
image: text().references(() => cdn.id),
|
||||
order: numeric().notNull(),
|
||||
});
|
||||
|
||||
export const tierlistVersions = tierlist.table("versions", {
|
||||
@@ -126,6 +132,8 @@ export const tierlistVersions = tierlist.table("versions", {
|
||||
.notNull()
|
||||
.references(() => tierlistTypes.id),
|
||||
image: text().references(() => cdn.id),
|
||||
from: text().notNull().references(()=>versions.id),
|
||||
order: numeric().notNull(),
|
||||
});
|
||||
|
||||
export const tierlistStates = tierlist.table("states", {
|
||||
@@ -138,7 +146,7 @@ export const tierlistStates = tierlist.table("states", {
|
||||
.references(() => tierlistVersions.id),
|
||||
tier: text()
|
||||
.notNull()
|
||||
.references(() => tierlistVersions.id),
|
||||
.references(() => tierlistTiers.id),
|
||||
column: text()
|
||||
.notNull()
|
||||
.references(() => tierlistColumns.id),
|
||||
@@ -147,6 +155,7 @@ export const tierlistStates = tierlist.table("states", {
|
||||
.references(() => tierlistBadges.id)
|
||||
.array(4)
|
||||
.default([]),
|
||||
order: numeric().notNull(),
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
+1
-1
@@ -92,4 +92,4 @@ export class EventSourceManager {
|
||||
}
|
||||
}
|
||||
|
||||
export const sse = new EventSourceManager();
|
||||
export const sse = new EventSourceManager();
|
||||
|
||||
+1
-1
@@ -20,5 +20,5 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/artifact/admin/:path*"], // Specify the routes the middleware applies to
|
||||
matcher: ["/artifact/admin/:path*", "/tl/:path*/:path*/admin"], // Specify the routes the middleware applies to
|
||||
};
|
||||
|
||||
+3
-1
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@bprogress/next": "^3.2.12",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@headless-tree/core": "^1.4.0",
|
||||
"@headless-tree/react": "^1.4.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
@@ -53,7 +54,7 @@
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "15.5.3",
|
||||
"next": "^15.5.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"pg": "^8.16.3",
|
||||
"radix-ui": "^1.4.3",
|
||||
@@ -65,6 +66,7 @@
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"uuidv7": "^1.0.2",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.1.11"
|
||||
|
||||
+27
-12
@@ -1,3 +1,4 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { AmberElementMap } from "@/lib/const";
|
||||
import { db } from "@/lib/db";
|
||||
import { cdn, characters, versions } from "@/lib/db/schema";
|
||||
@@ -10,16 +11,27 @@ async function main() {
|
||||
await db.transaction(async (tx) => {
|
||||
console.log("Cleaning up...");
|
||||
await tx.delete(characters);
|
||||
await tx.delete(cdn);
|
||||
await tx.delete(versions);
|
||||
console.log("Creating base version...");
|
||||
const [{ id: version }] = await tx
|
||||
.insert(versions)
|
||||
.values({
|
||||
id: "b10",
|
||||
name: "Base 1.0",
|
||||
})
|
||||
.returning({ id: versions.id });
|
||||
// await tx.delete(cdn);
|
||||
const existingBase = (
|
||||
await tx
|
||||
.select({ id: versions.id })
|
||||
.from(versions)
|
||||
.where(eq(versions.id, "b10"))
|
||||
).length;
|
||||
console.log(
|
||||
existingBase
|
||||
? "Base version already exist. Skipping creation."
|
||||
: "Creating base version...",
|
||||
);
|
||||
const [{ id: version }] = existingBase
|
||||
? await tx.select({ id: versions.id }).from(versions)
|
||||
: await tx
|
||||
.insert(versions)
|
||||
.values({
|
||||
id: "b10",
|
||||
name: "Base 1.0",
|
||||
})
|
||||
.returning({ id: versions.id });
|
||||
process.stdout.write("Syncing characters...");
|
||||
const total = Object.keys(data.data.items).length * 3;
|
||||
var done = 0;
|
||||
@@ -33,7 +45,7 @@ async function main() {
|
||||
);
|
||||
return p;
|
||||
}
|
||||
async function handle(char: Avatar) {
|
||||
async function handle(char: Avatar, order: string) {
|
||||
await tx
|
||||
.insert(characters)
|
||||
.values({
|
||||
@@ -56,14 +68,17 @@ async function main() {
|
||||
stars: char.rank as 4 | 5,
|
||||
version,
|
||||
weapon: char.weaponType,
|
||||
order,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
let i = 0;
|
||||
for (const char of Object.values(data.data.items)) {
|
||||
i += 10;
|
||||
process.stdout.write(
|
||||
`\rSyncing characters... ${char.name} `,
|
||||
);
|
||||
pros.push(handle(char).then(progress));
|
||||
pros.push(handle(char, `${i}`).then(progress));
|
||||
}
|
||||
await Promise.all(pros);
|
||||
console.log("\nDone!");
|
||||
|
||||
Reference in New Issue
Block a user