feat(art): #3 add edit button
This commit is contained in:
@@ -48,7 +48,7 @@ export default async function AdminLayout({
|
||||
queue: submissions.queue,
|
||||
})
|
||||
.from(submissions)
|
||||
.orderBy(submissions.id);
|
||||
.orderBy(submissions.queue);
|
||||
const config = await getArtifactConfig();
|
||||
return (
|
||||
<SidebarProvider>
|
||||
|
||||
@@ -44,15 +44,19 @@ import { RulesDialog } from "./rules";
|
||||
|
||||
export function CharacterChooser({
|
||||
clist,
|
||||
uid: defaultUid = "",
|
||||
char: defaultChar,
|
||||
}: {
|
||||
clist: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
uid?: string;
|
||||
char?: string;
|
||||
}) {
|
||||
const [uid, setUid] = useState("");
|
||||
const [uid, setUid] = useState(defaultUid);
|
||||
const [chars, setChars] = useState<(typeof characters.$inferSelect)[]>([]);
|
||||
const [selected, setSelected] = useState<string | undefined>();
|
||||
const [selected, setSelected] = useState<string | undefined>(defaultChar);
|
||||
const [isError, setIsError] = useState<boolean | string>(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [manual, setManual] = useState(false);
|
||||
@@ -60,6 +64,10 @@ export function CharacterChooser({
|
||||
const [guideDialogOpen, setGuideDialogOpen] = useState(false);
|
||||
const [cacheTtl, setCacheTtl] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultUid) setUid(defaultUid);
|
||||
}, [defaultUid]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
setIsError(false);
|
||||
@@ -83,10 +91,15 @@ export function CharacterChooser({
|
||||
c.avatarId.toString(),
|
||||
);
|
||||
const chars = await getCharacters(charIds);
|
||||
setChars(
|
||||
charIds.map((e) => chars.find((c) => c.amber === e)!).filter(Boolean),
|
||||
const userChars = charIds
|
||||
.map((e) => chars.find((c) => c.amber === e)!)
|
||||
.filter(Boolean);
|
||||
setChars(userChars);
|
||||
setSelected(
|
||||
userChars.some((c) => c.name === defaultChar)
|
||||
? defaultChar
|
||||
: undefined,
|
||||
);
|
||||
setSelected(undefined);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setIsError("ข้อผิดพลาดภายในระบบ");
|
||||
@@ -95,7 +108,7 @@ export function CharacterChooser({
|
||||
fetchChars()
|
||||
.catch(() => setIsError(true))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [uid]);
|
||||
}, [uid, defaultChar]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -108,6 +121,7 @@ export function CharacterChooser({
|
||||
required
|
||||
placeholder="814006303"
|
||||
onChange={(ev) => setUid(ev.target.value)}
|
||||
defaultValue={defaultUid}
|
||||
/>
|
||||
</div>
|
||||
<Label htmlFor="character">
|
||||
|
||||
@@ -4,10 +4,13 @@ import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { submitArtifact } from "@/lib/api";
|
||||
|
||||
export function ArtifactFormWrapper(props: React.ComponentProps<"form">) {
|
||||
export function ArtifactFormWrapper({
|
||||
edit,
|
||||
...props
|
||||
}: React.ComponentProps<"form"> & { edit?: { sub: string; token: string } }) {
|
||||
const router = useRouter();
|
||||
async function submit(data: FormData) {
|
||||
submitArtifact(data)
|
||||
submitArtifact(data, edit)
|
||||
.then((q) => {
|
||||
if (typeof q === "string") return toast.error(q);
|
||||
try {
|
||||
|
||||
+57
-13
@@ -3,6 +3,7 @@ import { eq } from "drizzle-orm";
|
||||
import {
|
||||
BookAlert,
|
||||
CircleDollarSign,
|
||||
PencilIcon,
|
||||
SendHorizonal,
|
||||
SquarePlay,
|
||||
} from "lucide-react";
|
||||
@@ -46,14 +47,13 @@ export const metadata: Metadata = {
|
||||
description: "ระบบลงคิวดูอาร์ติแฟกต์เกนชินในไลฟ์สตรีม",
|
||||
};
|
||||
|
||||
export default async function ArtifactFormPage() {
|
||||
export default async function ArtifactFormPage({
|
||||
searchParams,
|
||||
}: PageProps<"/artifact">) {
|
||||
const cookie = await cookies();
|
||||
const sid = cookie.get("sid");
|
||||
const [q] = sid?.value
|
||||
? await db
|
||||
.select({ queue: submissions.queue })
|
||||
.from(submissions)
|
||||
.where(eq(submissions.id, sid.value))
|
||||
? await db.select().from(submissions).where(eq(submissions.id, sid.value))
|
||||
: [];
|
||||
const clist = await db
|
||||
.select({
|
||||
@@ -64,6 +64,8 @@ export default async function ArtifactFormPage() {
|
||||
.orderBy(characters.name);
|
||||
const config = await getArtifactConfig();
|
||||
const count = await db.$count(submissions);
|
||||
const { edit: searchEdit } = await searchParams;
|
||||
const editing = q && searchEdit === q.editToken && q.edits < 5;
|
||||
|
||||
return (
|
||||
<div className="flex justify-around items-center h-svh">
|
||||
@@ -71,12 +73,37 @@ export default async function ArtifactFormPage() {
|
||||
<div className="aspect-video w-110"></div>
|
||||
</div>
|
||||
<Card className="w-full max-w-md">
|
||||
{q ? (
|
||||
{editing ? (
|
||||
""
|
||||
) : 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>
|
||||
{q.edits < 5 && !q.checked ? (
|
||||
<SimpleTooltip text="แก้ไข">
|
||||
<Button
|
||||
className="absolute bottom-0 right-0 m-2"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
asChild
|
||||
>
|
||||
<Link href={`?edit=${q.editToken}`}>
|
||||
<PencilIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
</SimpleTooltip>
|
||||
) : (
|
||||
<Button
|
||||
className="absolute bottom-0 right-0 m-2 bg-red-500/50!"
|
||||
variant="outline"
|
||||
disabled
|
||||
>
|
||||
<PencilIcon />
|
||||
แก้ไม่ได้แล้ว
|
||||
</Button>
|
||||
)}
|
||||
</Blocker>
|
||||
) : config.locked ? (
|
||||
<Blocker>
|
||||
@@ -111,7 +138,10 @@ export default async function ArtifactFormPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ArtifactFormWrapper id="mainform">
|
||||
<ArtifactFormWrapper
|
||||
id="mainform"
|
||||
edit={q ? { token: q.editToken, sub: q.id } : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">ชื่อ*</Label>
|
||||
@@ -123,10 +153,15 @@ export default async function ArtifactFormPage() {
|
||||
autoComplete="name"
|
||||
maxLength={64}
|
||||
required
|
||||
defaultValue={editing ? q.name : undefined}
|
||||
/>
|
||||
</div>
|
||||
{config.enka ? (
|
||||
<CharacterChooser clist={clist} />
|
||||
editing ? (
|
||||
<CharacterChooser clist={clist} uid={q.uid} char={q.char} />
|
||||
) : (
|
||||
<CharacterChooser clist={clist} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
@@ -138,6 +173,7 @@ export default async function ArtifactFormPage() {
|
||||
required
|
||||
placeholder="814006303"
|
||||
maxLength={10}
|
||||
defaultValue={editing ? q.uid : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
@@ -148,6 +184,7 @@ export default async function ArtifactFormPage() {
|
||||
name="character"
|
||||
data={clist}
|
||||
className="w-full bg-transparent! hover:bg-accent!"
|
||||
defaultValue={editing ? q.char : undefined}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -160,6 +197,7 @@ export default async function ArtifactFormPage() {
|
||||
placeholder="เช่น Er พอไหมครับ, คริสวยยังครับ (ไม่บังคับ)"
|
||||
className="bg-card!"
|
||||
maxLength={1024}
|
||||
defaultValue={editing ? q.comment : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,9 +234,15 @@ export default async function ArtifactFormPage() {
|
||||
</RulesDialog>
|
||||
<TooltipContent>อ่านกฏการลงคิว</TooltipContent>
|
||||
</Tooltip>
|
||||
<Suspense>
|
||||
<LiveButton />
|
||||
</Suspense>
|
||||
{editing ? (
|
||||
<Button variant="destructive" asChild>
|
||||
<Link href="?">ยกเลิก</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Suspense>
|
||||
<LiveButton />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="p-1">
|
||||
@@ -209,12 +253,12 @@ export default async function ArtifactFormPage() {
|
||||
type="submit"
|
||||
form="mainform"
|
||||
disabled={
|
||||
!!q ||
|
||||
(!!q && !editing) ||
|
||||
config.locked ||
|
||||
(config.limit >= 0 && count >= config.limit)
|
||||
}
|
||||
>
|
||||
<SendHorizonal />
|
||||
{editing ? <PencilIcon /> : <SendHorizonal />}
|
||||
</Button>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ import { cn } from "@/lib/utils";
|
||||
export function Navbar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (!/^\/(?:tl(?:\/[a-zA-Z0-9]+)?|rubgram|artifact)$/gm.test(pathname)) return "";
|
||||
if (!/^\/(?:tl(?:\/[a-zA-Z0-9]+)?|rubgram|artifact)$/gm.test(pathname))
|
||||
return "";
|
||||
return (
|
||||
<div className="flex gap-2 p-2 rounded-lg absolute top-3 left-3 border bg-card">
|
||||
<Item icon={<Home size={16} />} name="หน้าหลัก" href="/" divide />
|
||||
|
||||
@@ -13,10 +13,7 @@ import { Spinner } from "@/components/ui/spinner";
|
||||
import { debugUploadSlip, discordCall } from "../../api";
|
||||
|
||||
export function Evernight() {
|
||||
return (
|
||||
<div className="h-full w-full relative">
|
||||
</div>
|
||||
);
|
||||
return <div className="h-full w-full relative"></div>;
|
||||
}
|
||||
|
||||
export function CopyButton({ text }: { text: string }) {
|
||||
|
||||
@@ -129,7 +129,7 @@ export default async function AdminSubmissionView({
|
||||
)}
|
||||
</div>
|
||||
<div className="border rounded-md h-full overflow-hidden">
|
||||
<Evernight/>
|
||||
<Evernight />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Vendored
+2
@@ -24,6 +24,8 @@ export interface TypedFormData<T extends TypedFormDataShape> {
|
||||
|
||||
has<K extends keyof T>(key: K): boolean;
|
||||
|
||||
entries(): T;
|
||||
|
||||
/** underlying raw FormData */
|
||||
readonly raw: FormData;
|
||||
}
|
||||
|
||||
@@ -666,7 +666,7 @@ export function TierList({
|
||||
className="flex gap-2 items-center justify-center"
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
>
|
||||
<SimpleTooltip text="เพิ่มตัวละคร">
|
||||
<SimpleTooltip text="เพิ่มตัวละคร">
|
||||
<span>
|
||||
<ComboBox
|
||||
placeholder="ค้นหาตัวละคร"
|
||||
@@ -689,7 +689,11 @@ export function TierList({
|
||||
/>
|
||||
</span>
|
||||
</SimpleTooltip>
|
||||
<SimpleTooltip text={deleteMode ? "คลิกอีกครั้งเพื่อออกจากโหมดลบ" : "ลบตัวละครออก"}>
|
||||
<SimpleTooltip
|
||||
text={
|
||||
deleteMode ? "คลิกอีกครั้งเพื่อออกจากโหมดลบ" : "ลบตัวละครออก"
|
||||
}
|
||||
>
|
||||
<Trash2
|
||||
className={cn(
|
||||
"pointer-events-auto! text-red-500",
|
||||
|
||||
@@ -12,7 +12,9 @@ export const metadata: Metadata = {
|
||||
|
||||
export default async function TierlistSelectionPage({
|
||||
params,
|
||||
}: { params: Promise<{ type: string }> }) {
|
||||
}: {
|
||||
params: Promise<{ type: string }>;
|
||||
}) {
|
||||
const { type: typeId } = await params;
|
||||
const types = await db
|
||||
.select()
|
||||
@@ -38,25 +40,24 @@ export default async function TierlistSelectionPage({
|
||||
{t.mode}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
|
||||
{t.versions.map((e) => (
|
||||
<Link href={`/tl/${t.id}/${e.id}`} key={e.id}>
|
||||
<Link href={`/tl/${t.id}/${e.id}`} key={e.id}>
|
||||
{e.image ? (
|
||||
<div className="relative aspect-video w-full rounded-lg border overflow-hidden">
|
||||
<Image
|
||||
src={`/cdn/${e.image}`}
|
||||
alt={e.name}
|
||||
fill
|
||||
className="rounded-sm border object-cover bg-primary"
|
||||
className="rounded-sm border object-cover bg-primary"
|
||||
/>
|
||||
<div className="flex absolute w-full justify-center bottom-0 py-1 bg-black/50 rounded-b-sm">
|
||||
{e.name}
|
||||
<div className="flex absolute w-full justify-center bottom-0 py-1 bg-black/50 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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
+14
-14
@@ -41,21 +41,21 @@ export default async function TierlistSelectionPage() {
|
||||
</center>
|
||||
{vers.map((t) => (
|
||||
<div className="flex flex-col gap-1" key={t.id}>
|
||||
<Link href={`/tl/${t.id}`} className="font-bold text-4xl w-fit">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="px-2 py-1 border rounded-md bg-[#2228]">
|
||||
{t.name}
|
||||
<span className="text-sm text-muted-foreground ml-2">
|
||||
{t.mode}
|
||||
</span>
|
||||
</div>
|
||||
<Link href={`/tl/${t.id}`} className="font-bold text-4xl w-fit">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="px-2 py-1 border rounded-md bg-[#2228]">
|
||||
{t.name}
|
||||
<span className="text-sm text-muted-foreground ml-2">
|
||||
{t.mode}
|
||||
</span>
|
||||
</div>
|
||||
<SimpleTooltip text="ดูเทียร์ลิสต์ของคอนเทนต์นี้ทั้งหมด">
|
||||
<span className="text-sm text-muted-foreground ml-2">
|
||||
ดูทั้งหมด
|
||||
</span>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
</Link>
|
||||
<span className="text-sm text-muted-foreground ml-2">
|
||||
ดูทั้งหมด
|
||||
</span>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
</Link>
|
||||
<HorizontalDiv>
|
||||
<div className="flex gap-2 max-w-full">
|
||||
{t.versions.map((e) => (
|
||||
|
||||
@@ -26,6 +26,7 @@ export function ComboBox({
|
||||
className,
|
||||
trigger,
|
||||
onValueSelect,
|
||||
defaultValue = "",
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
data: { value: string; label: string }[];
|
||||
@@ -34,7 +35,11 @@ export function ComboBox({
|
||||
onValueSelect?: (val: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, setValue] = React.useState("");
|
||||
const [value, setValue] = React.useState(defaultValue);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (defaultValue) setValue(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+73
-33
@@ -1,10 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { and, eq, inArray, isNotNull, not, or, sql } from "drizzle-orm";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import { and, eq, inArray, isNotNull, lt, not, or, sql } from "drizzle-orm";
|
||||
import type { PgDatabase } from "drizzle-orm/pg-core";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
||||
import z from "zod";
|
||||
import { adminCheck } from "./auth";
|
||||
import { uidRegex } from "./const";
|
||||
import { db } from "./db";
|
||||
@@ -37,45 +38,84 @@ export async function getArtifactConfig() {
|
||||
return { locked: false, limit: -1, enka: false, ...art, ...glob };
|
||||
}
|
||||
|
||||
export async function submitArtifact(formData: FormData) {
|
||||
const form = formData as unknown as TypedFormData<{
|
||||
name: string;
|
||||
uid: string;
|
||||
character: string;
|
||||
comment: string;
|
||||
}>;
|
||||
const ArtifactSubmission = z.object(
|
||||
{
|
||||
name: z.string().max(64, "ชื่อยาวเกินไป ต้องไม่เกิน 64 ตัวอักษร"),
|
||||
uid: z
|
||||
.string()
|
||||
.regex(uidRegex, "UID ไม่ถูกต้อง ต้องเป็นเลข 9 หรือ 10 หลัก เท่านั้น")
|
||||
.refine(
|
||||
(uid) =>
|
||||
db
|
||||
.select()
|
||||
.from(submissions)
|
||||
.where(eq(submissions.uid, uid))
|
||||
.limit(1)
|
||||
.then((r) => !!r.length),
|
||||
"คุณลงทะเบียนไปแล้ว",
|
||||
),
|
||||
character: z.string().refine(
|
||||
(char) =>
|
||||
db
|
||||
.select()
|
||||
.from(characters)
|
||||
.where(eq(characters.name, char))
|
||||
.limit(1)
|
||||
.then((r) => !!r.length),
|
||||
"ไม่พบตัวละครที่เลือก",
|
||||
),
|
||||
comment: z.string().max(1024, "ข้อความเพิ่มเติมยาวเกินไป ต้องไม่เกิน 1024 ตัวอักษร"),
|
||||
},
|
||||
"กรุณากรอกข้อมูลให้ครบถ้วน",
|
||||
);
|
||||
|
||||
export async function submitArtifact(
|
||||
formData: FormData,
|
||||
edit?: { sub: string; token: string },
|
||||
) {
|
||||
const config = await getArtifactConfig();
|
||||
const name = form.get("name")?.toString();
|
||||
const uid = form.get("uid")?.toString();
|
||||
const character = form.get("character")?.toString();
|
||||
const comment = form.get("comment")?.toString() || "";
|
||||
if (!name || !uid || !character) return "กรุณากรอกข้อมูลให้ครบถ้วน";
|
||||
if (name.length > 64) return "ชื่อยาวเกินไป ต้องไม่เกิน 64 ตัวอักษร";
|
||||
if (!uidRegex.test(uid)) return "UID ไม่ถูกต้อง ต้องเป็นเลข 9 หรือ 10 หลัก เท่านั้น";
|
||||
if (comment.length > 1024) return "ข้อความเพิ่มเติมยาวเกินไป ต้องไม่เกิน 1024 ตัวอักษร";
|
||||
if (config.locked) return "ปิดรับลงทะเบียนชั่วคราว เนื่องจากมีผู้ลงจำนวนมาก";
|
||||
const count = await db.$count(submissions);
|
||||
if (config.limit !== -1 && count >= config.limit)
|
||||
return `คิวลงทะเบียนเต็มแล้ว (${config.limit} ครั้ง)`;
|
||||
const [char] = await db
|
||||
.select()
|
||||
.from(characters)
|
||||
.where(eq(characters.name, character))
|
||||
.limit(1);
|
||||
if (!char) return "ไม่พบตัวละครที่เลือก";
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(submissions)
|
||||
.where(eq(submissions.uid, uid))
|
||||
.limit(1);
|
||||
if (existing) return "คุณลงทะเบียนไปแล้ว";
|
||||
const { success, data, error } = await ArtifactSubmission.safeParseAsync(
|
||||
Object.fromEntries(formData.entries()),
|
||||
);
|
||||
if (!success) return z.prettifyError(error);
|
||||
if (edit) {
|
||||
const [existing] = await db
|
||||
.delete(submissions)
|
||||
.where(
|
||||
and(
|
||||
eq(submissions.id, edit.sub),
|
||||
eq(submissions.editToken, edit.token),
|
||||
lt(submissions.edits, 5),
|
||||
not(submissions.checked),
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!existing) return "คิวนี้แก้ไม่ได้แล้ว";
|
||||
|
||||
const [queue] = await db
|
||||
.insert(submissions)
|
||||
.values({
|
||||
...data,
|
||||
char: data.character,
|
||||
queue: existing.queue,
|
||||
edits: existing.edits + 1,
|
||||
editToken: randomUUIDv7(),
|
||||
})
|
||||
.returning({ queue: submissions.queue, id: submissions.id });
|
||||
revalidatePath("/artifact/admin");
|
||||
ps.publish({ type: "submit" }, { topic: "artifact", event: "update" });
|
||||
return queue;
|
||||
}
|
||||
const [queue] = await db
|
||||
.insert(submissions)
|
||||
.values({
|
||||
uid,
|
||||
name,
|
||||
comment,
|
||||
char: char.name,
|
||||
...data,
|
||||
char: data.character,
|
||||
})
|
||||
.returning({ queue: submissions.queue, id: submissions.id });
|
||||
revalidatePath("/artifact/admin");
|
||||
|
||||
@@ -16,6 +16,7 @@ type SharedStates = Partial<{
|
||||
updated: boolean;
|
||||
"rubgram.services": string[];
|
||||
"tl.deleteMode": boolean;
|
||||
inspect: Record<string, string>;
|
||||
_raw: Record<string, unknown>;
|
||||
}>;
|
||||
|
||||
@@ -165,4 +166,23 @@ export const shared = {
|
||||
raw() {
|
||||
return [shared.state("_raw")[0]];
|
||||
},
|
||||
inspect(k: string, v: Parameters<typeof console.log>[0]) {
|
||||
const comms = use(IccContext);
|
||||
// unmount handler
|
||||
useEffect(
|
||||
() => () => {
|
||||
const { [k]: removed, ...rest } = comms.get("inspect") || {};
|
||||
comms.set("inspect", rest);
|
||||
},
|
||||
[],
|
||||
);
|
||||
// value updater
|
||||
useEffect(() => {
|
||||
const obj = comms.get("inspect") || {};
|
||||
comms.set("inspect", {
|
||||
...obj,
|
||||
[k]: typeof v === "string" ? v : JSON.stringify(v),
|
||||
});
|
||||
}, [v]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -94,6 +94,8 @@ export const submissions = artifact.table("submissions", {
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
queue: serial(),
|
||||
editToken: text().notNull().$defaultFn(uuidv7),
|
||||
edits: integer().notNull().default(0),
|
||||
checked: boolean().notNull().default(false),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user