Files
buzz/app/(ui)/admin/@modal/char/create/page.tsx
T
dmgnr be0e496b45
Build and Push Image / build-frontend (push) Successful in 8m40s
Build and Push Image / push (push) Has been cancelled
Build and Push Image / build-backend (push) Has been cancelled
fix: mark all server-side modal routes as force-dynamic
2026-06-15 05:15:27 +00:00

218 lines
7.2 KiB
TypeScript
Executable File

import { desc, sql } from "drizzle-orm";
import { ArrowRight, PlusIcon } from "lucide-react";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { CdnChooser } from "@/components/chooser";
import { FormAction, FormInput, FormProvider } from "@/components/form";
import { ModalBase } from "@/components/modal";
import { Button } from "@/components/ui/button";
import { DialogFooter } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Spinner } from "@/components/ui/spinner";
import { actionLog } from "@/lib/api";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import type { element } from "@/lib/db/schema";
import { characters, element as elementEnum, versions } from "@/lib/db/schema";
import { genId } from "./overrides";
export default async function CharacterCreatePage() {
const [[{ maxOrder }], versionList] = await Promise.all([
db
.select({ maxOrder: sql<number>`MAX(${characters.order})` })
.from(characters),
db.select().from(versions).orderBy(desc(versions.id)),
]);
async function submit(form: FormData) {
"use server";
if (!(await adminCheck())) redirect("/login");
for (const field of [
"name",
"id",
"element",
"stars",
"image",
"order",
"version",
"weapon",
"amber",
] as const) {
if (!form.get(field)) {
return { error: `Field "${field}" is required.` };
}
}
let data: typeof characters.$inferInsert;
try {
data = {
id: form.get("id") as string,
name: form.get("name") as string,
vision: form.get("element") as (typeof element.enumValues)[number],
stars: parseInt((form.get("stars") as string) || "0", 10) as 4 | 5,
image: form.get("image") as string,
order: parseInt(form.get("order") as string, 10),
version: form.get("version") as string,
weapon: form.get("weapon") as string,
amber: form.get("amber") as string,
};
await db.insert(characters).values(data);
} catch (e) {
console.error(e);
const err = e as Error & { cause: { detail: string; message: string } };
return {
error:
err?.cause?.detail ||
err?.cause?.message ||
"Failed to update character in database.",
};
}
await actionLog(`Created character ${data.id}`, data);
revalidatePath("/admin/char");
return { toast: "Character created successfully.", close: true };
}
return (
<ModalBase title="Create Character">
<FormProvider id="char-create" onSubmit={submit}>
<div className="flex gap-2">
<div className="flex grow flex-col gap-2">
<FormInput name="name" override={genId} label="Name">
<Input placeholder="Traveler (Electro)" autoFocus />
</FormInput>
</div>
<div className="flex grow flex-col gap-2">
<FormInput name="id" label="ID">
<Input placeholder="traveler_electro (Automatic)" />
</FormInput>
</div>
</div>
<div className="flex gap-2">
<div className="flex grow flex-col gap-2">
<FormInput name="element" label="Element">
<Select>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an Element" />
</SelectTrigger>
<SelectContent>
{elementEnum.enumValues.map((el) => (
<SelectItem key={el} value={el}>
{el.charAt(0).toUpperCase() + el.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</FormInput>
</div>
<div className="flex flex-col gap-2">
<FormInput name="stars" label="Stars">
<Select>
<SelectTrigger className="w-min">
<SelectValue placeholder=" " />
</SelectTrigger>
<SelectContent>
<SelectItem value="4">4</SelectItem>
<SelectItem value="5">5</SelectItem>
</SelectContent>
</Select>
</FormInput>
</div>
</div>
<FormInput name="order" label="Order">
<Input placeholder="1" defaultValue={maxOrder + 10} />
</FormInput>
<FormInput name="version" label="Version">
<Select>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a version" />
</SelectTrigger>
<SelectContent>
{versionList.map((version) => (
<SelectItem key={version.id} value={version.id}>
{version.name}{" "}
<KbdGroup>
<Kbd>{version.id}</Kbd>
{version.from && (
<Kbd>
<ArrowRight />
{version.from}
</Kbd>
)}
</KbdGroup>
</SelectItem>
))}
</SelectContent>
</Select>
</FormInput>
<FormInput name="image" label="Image">
<CdnChooser />
</FormInput>
<Separator />
<div className="flex gap-2">
<div className="flex grow flex-col gap-2">
<FormInput name="weapon" label="Weapon" subLabel="(for Amber)">
<Select>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a weapon" />
</SelectTrigger>
<SelectContent>
{Object.entries({
WEAPON_SWORD_ONE_HAND: "Sword",
WEAPON_CATALYST: "Catalyst",
WEAPON_CLAYMORE: "Claymore",
WEAPON_BOW: "Bow",
WEAPON_POLE: "Polearm",
}).map(([id, weapon]) => (
<SelectItem key={id} value={id}>
{weapon}
</SelectItem>
))}
</SelectContent>
</Select>
</FormInput>
</div>
<div className="flex grow flex-col gap-2">
<FormInput name="amber" label="Amber ID">
<Input placeholder="10000005-electro" />
</FormInput>
</div>
</div>
<DialogFooter>
<FormAction type="clear" asChild>
<Button variant="outline">Clear</Button>
</FormAction>
<Button asChild>
<FormAction
type="submit"
loading={
<>
<Spinner />
Creating...
</>
}
>
<PlusIcon />
Create
</FormAction>
</Button>
</DialogFooter>
</FormProvider>
</ModalBase>
);
}
export const dynamic = "force-dynamic";