Restructure layout and add widget

This commit is contained in:
Dreamgineer
2025-11-12 20:52:50 -05:00 Unverified
parent b831e2f2a1
commit f2551e5d51
51 changed files with 181 additions and 121 deletions
+46
View File
@@ -0,0 +1,46 @@
"use client";
import { Copy, CopyCheck, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function EnkaBrowser({ uid }: { uid: string }) {
const [id, setId] = useState(uid);
const [ready, setReady] = useState(false);
useEffect(() => {
setId(uid);
setReady(false);
}, [uid]);
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" />
)}
<iframe
src={`https://enka.network/u/${id}`}
className={cn(
"w-full border-0 bg-card -mt-20 h-[calc(100%+5rem)]",
!ready && "grayscale blur-md brightness-50 pointer-events-none",
)}
title="Enka Network"
onLoad={() => setReady(true)}
/>
</div>
);
}
export function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
async function copy() {
if (copied) return;
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<Button variant="ghost" size="icon" onClick={copy} disabled={copied}>
{copied ? <CopyCheck /> : <Copy />}
</Button>
);
}
+56
View File
@@ -0,0 +1,56 @@
import { eq } from "drizzle-orm";
import Avatar from "@/components/avatar";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { db } from "@/lib/db";
import { characters, submissions } from "@/lib/db/schema";
import { CopyButton, EnkaBrowser } from "./client";
export default async function AdminSubmissionView({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const [sub] = await db
.select()
.from(submissions)
.where(eq(submissions.id, id))
.limit(1);
const [char] = await db
.select()
.from(characters)
.where(eq(characters.name, sub?.char || ""))
.limit(1);
return (
<div className="p-2 h-full">
<div className="flex flex-col h-full w-full gap-2">
<div className="flex w-full justify-between gap-2">
<Card className="w-full pb-1 rounded-md">
<CardHeader>
<CardTitle>
{sub.queue}. {sub.name}
</CardTitle>
<CardDescription>{sub.comment}</CardDescription>
</CardHeader>
<CardFooter className="h-full items-end">
<div className="flex items-center">
<span className="text-gray-500">{sub.uid}</span>
<CopyButton text={sub.uid} />
</div>
</CardFooter>
</Card>
<Avatar char={char} />
</div>
<div className="border rounded-md h-full overflow-hidden">
<EnkaBrowser uid={sub?.uid || ""} />
</div>
</div>
</div>
);
}
+216
View File
@@ -0,0 +1,216 @@
"use client";
import { useProgress } from "@bprogress/next";
import { Lock, Unlock } from "lucide-react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { setLimit, toggleCheck, toggleLock } from "@/lib/api";
import { cn } from "@/lib/utils";
export function SidebarLink({
submission,
className,
}: {
submission: { id: string; name: string; checked: boolean; queue: number };
className?: string;
}) {
const { stop } = useProgress();
const { id } = useParams();
const [checked, setChecked] = useState(submission.checked);
useEffect(() => {
setChecked(submission.checked);
}, [submission.checked]);
return (
<Link
href={`/artifact/admin/${submission.id}`}
className={cn(
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={checked}
onCheckedChange={async () => {
setChecked((x) => !x);
await toggleCheck(submission.id);
stop();
}}
/>
</SidebarMenuBadge>
</Link>
);
}
export function LimitManager({
config,
length,
...props
}: React.ComponentProps<typeof SidebarMenu> & {
config: Awaited<ReturnType<typeof import("@/lib/api").getArtifactConfig>>;
length: number;
}) {
return (
<SidebarMenu {...props}>
<SidebarMenuItem>
<Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton>
<Tooltip>
{config.locked ? (
<>
<TooltipTrigger asChild>
<Lock className="text-red-500" />
</TooltipTrigger>
<TooltipContent>
<p>ปิดรับ</p>
</TooltipContent>
</>
) : config.limit >= 0 && length > config.limit ? (
<>
<TooltipTrigger asChild>
<Lock className="text-orange-400" />
</TooltipTrigger>
<TooltipContent>
<p>เต็มแล้ว</p>
</TooltipContent>
</>
) : (
<>
<TooltipTrigger asChild>
<Unlock className="text-sky-400" />
</TooltipTrigger>
<TooltipContent>
<p>เปิดรับ</p>
</TooltipContent>
</>
)}
</Tooltip>{" "}
จำนวน
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="end">
<DialogTrigger asChild>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<span>ตั้ง Limit</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Set limit</DialogTitle>
</DialogHeader>
<form
id="limit-form"
action={async (data: FormData) =>
setLimit(Number(data.get("limit")) || -1)
}
>
<Input
id="limit"
name="limit"
type="number"
placeholder="ไม่จำกัด"
defaultValue={config.limit >= 0 ? config.limit : ""}
/>
</form>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button type="submit" form="limit-form">
Save changes
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
<DropdownMenuItem onClick={toggleLock}>
<span>{config.locked && "เ"}ปิดรับ</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</Dialog>
<SidebarMenuBadge>
{length}
{config.limit >= 0 ? `/${config.limit}` : ""}
</SidebarMenuBadge>
</SidebarMenuItem>
</SidebarMenu>
);
}
export function SubmissionList({
subs,
}: {
subs: { id: string; name: string; checked: boolean; queue: number }[];
}) {
const [query, setQuery] = useState("");
return (
<>
<Input
placeholder="Search..."
type="search"
onChange={(ev) => setQuery(ev.target.value.toLowerCase())}
/>
{subs
.filter((s) => (s.queue + s.name).toLowerCase().includes(query))
.map((s) => (
<SidebarMenuButton key={s.id} asChild>
<SidebarLink submission={s} />
</SidebarMenuButton>
))}
</>
);
}
export function Watcher() {
const router = useRouter();
useEffect(() => {
const es = new EventSource(`/api/artifact/ev`);
es.addEventListener("update", () => router.refresh());
return () => es.close();
}, [router]);
return <div></div>;
}
+111
View File
@@ -0,0 +1,111 @@
import { Dice3, Trash2 } from "lucide-react";
import { redirect } from "next/navigation";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
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";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
if (!(await apiAuthCheck())) redirect("/");
const subs = await db
.select({
id: submissions.id,
name: submissions.name,
checked: submissions.checked,
queue: submissions.queue,
})
.from(submissions)
.orderBy(submissions.id);
const config = await getArtifactConfig();
return (
<SidebarProvider>
<Sidebar>
<SidebarHeader>
<div className="flex justify-between items-center">
<b>Admin</b>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={random}
>
<Dice3 size={24} className="size-6" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<Trash2 size={24} className="size-6 text-red-500" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="z-101">
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete
your account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={wipe}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Submissions</SidebarGroupLabel>
<SidebarMenu>
<SubmissionList subs={subs} />
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarGroup>
<SidebarGroupLabel>การตั้งค่า</SidebarGroupLabel>
<LimitManager config={config} length={subs.length} />
</SidebarGroup>
</SidebarFooter>
</Sidebar>
<SidebarInset className="bg-transparent">
{children}
<Watcher />
</SidebarInset>
</SidebarProvider>
);
}
@@ -0,0 +1,14 @@
import generate from "@/lib/og";
// Image metadata
export const size = {
width: 1222,
height: 560,
};
export const contentType = "image/png";
// Image generation
export default async function Image() {
return generate({ title: "เสือกไอดีชาวบ้าน", sub: "(ฝั่งแอดมิน)" });
}
+7
View File
@@ -0,0 +1,7 @@
export const metadata = {
title: "เสือกไอดีชาวบ้าน (แอดมิน)",
};
export default function AdminPage() {
return <div></div>;
}
+187
View File
@@ -0,0 +1,187 @@
/** biome-ignore-all lint/a11y/noStaticElementInteractions: Span is needed for double triggering */
/** biome-ignore-all lint/a11y/useKeyWithClickEvents: Not needed */
"use client";
import { BookAlert, CircleX, Loader2, Search } from "lucide-react";
import { useEffect, useState } from "react";
import Avatar from "@/components/avatar";
import { Blocker } from "@/components/blocker";
import { ComboBox } from "@/components/combobox";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { getCharacters } from "@/lib/api";
import { comms } from "@/lib/comms";
import { uidRegex } from "@/lib/const";
import type { characters } from "@/lib/db/schema";
import { cn } from "@/lib/utils";
import type { EnkaNetworkUser } from "@/types/enka";
import { RulesDialog } from "./rules";
export function CharacterChooser({
clist,
}: {
clist: {
value: string;
label: string;
}[];
}) {
const [uid, setUid] = useState("");
const [chars, setChars] = useState<(typeof characters.$inferSelect)[]>([]);
const [selected, setSelected] = useState<string | undefined>();
const [isError, setIsError] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [manual, setManual] = useState(false);
useEffect(() => {
setIsLoading(true);
setIsError(false);
async function fetchChars() {
if (!uidRegex.test(uid)) return setChars([]);
const res = await fetch(`/api/enka/${uid}`);
if (!res.ok) return setIsError(true);
const data: EnkaNetworkUser = await res.json();
if (!data.playerInfo.showAvatarInfoList) return setIsError(true);
const charIds = data.playerInfo.showAvatarInfoList.map((c) =>
c.avatarId.toString(),
);
const chars = await getCharacters(charIds);
setChars(
charIds.map((e) => chars.find((c) => c.amber === e)!).filter(Boolean),
);
}
fetchChars()
.catch(() => setIsError(true))
.finally(() => setIsLoading(false));
}, [uid]);
return (
<>
<div className="grid gap-2">
<Label htmlFor="uid">UID</Label>
<Input
id="uid"
name="uid"
type="number"
required
placeholder="814006303"
onChange={(ev) => setUid(ev.target.value)}
/>
</div>
<Label htmlFor="character">
เลือกตัวละครที่ต้องการ
{manual ? (
<u
className="ml-auto cursor-pointer"
onClick={() => setManual(false)}
>
เลือกจากลิสต์
</u>
) : (
<span className="ml-auto">
หาไม่เจอ?{" "}
<u className="cursor-pointer" onClick={() => setManual(true)}>
เลือกเอง
</u>
</span>
)}
</Label>
{manual ? (
<ComboBox
placeholder="ค้นหาตัวละคร"
id="character"
name="character"
data={clist}
className="w-full bg-transparent! hover:bg-accent!"
/>
) : (
<ScrollArea>
{isError ? (
<Placeholder>
<CircleX className="text-red-500" /> เกิดข้อผิดพลาดในการดึงข้อมูล
</Placeholder>
) : isLoading ? (
<Placeholder>
<Loader2 className="animate-spin" /> กำลังโหลดตัวละคร...
</Placeholder>
) : !chars.length ? (
<div className="flex gap-2 mb-2">
<Placeholder />
<Placeholder />
<Placeholder />
<Placeholder />
<Placeholder />
</div>
) : (
<div className="flex gap-2 mb-2">
<Placeholder
className="w-[76.8px] cursor-pointer rounded-sm"
onClick={() => setManual(true)}
>
<Search />
</Placeholder>
{chars.map((c) => (
<Button key={c.id} asChild onClick={() => setSelected(c.name)}>
<Avatar
scale={0.6}
char={c}
selected={selected ? selected === c.name : 0}
/>
</Button>
))}
</div>
)}
<ScrollBar orientation="horizontal" />
<input
id="character"
name="character"
type="hidden"
value={selected}
/>
</ScrollArea>
)}
</>
);
}
function Placeholder({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"w-full h-[100.8px] flex gap-2 justify-center items-center rounded-md bg-muted",
className,
)}
{...props}
></div>
);
}
export function Disclaimer() {
const [agreed, setAgreed] = useState(false);
if (agreed) return;
return (
<Blocker>
<RulesDialog onOpenChange={(set) => !set && setAgreed(true)}>
<Button variant="destructive" type="button">
<BookAlert /> อ่านกฎ
</Button>
</RulesDialog>
</Blocker>
);
}
export async function ClearCookie() {
// biome-ignore lint/suspicious/noTsIgnore: typescript issue
// @ts-ignore
cookieStore.delete("sid");
return "";
}
export function WarningDialog() {
const [manual, setManual] = comms.var("manual");
comms.event("beforeSubmit", () => {
manual;
});
}
+31
View File
@@ -0,0 +1,31 @@
"use client";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { submitArtifact } from "@/lib/api";
export function ArtifactFormWrapper(props: React.ComponentProps<"form">) {
const router = useRouter();
async function submit(data: FormData) {
submitArtifact(data)
.then((q) => {
if (typeof q === "string") return toast.error(q);
try {
// biome-ignore lint/suspicious/noTsIgnore: typescript issue
// @ts-ignore
cookieStore.set("sid", q.id);
} catch {}
router.refresh();
})
.catch((e) => toast.error(`${e.message || e}`));
}
return (
<form
{...props}
onSubmit={(e) => {
e.preventDefault();
submit(new FormData(e.target as HTMLFormElement));
}}
/>
);
}
+46
View File
@@ -0,0 +1,46 @@
import { TooltipContent, TooltipTrigger } from "@radix-ui/react-tooltip";
import { TvMinimalPlay } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import type { YoutubeLiveInfo } from "@/app/api/live/route";
import { Button } from "@/components/ui/button";
import { Tooltip } from "@/components/ui/tooltip";
export async function LiveButton() {
const live: YoutubeLiveInfo = await fetch(`http://localhost:3000/api/live`)
.then((e) => e.json())
.catch(() => "none");
if (live === "none") return;
const { url, thumbnails, title } = live;
const targetWidth = 220;
return (
<Tooltip>
<TooltipTrigger asChild>
<Link href={url}>
<Button variant="outline" type="button">
<TvMinimalPlay className="text-red-500 animate-pulse" /> LIVE
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<div className="m-2 p-2 flex flex-col gap-2 rounded border bg-card">
<Image
height={thumbnails.height * (targetWidth / thumbnails.width)}
width={targetWidth}
src={thumbnails.url}
alt="yt thumbnail"
className="rounded border"
/>
<div
className="truncate line-clamp-2"
style={{ maxWidth: targetWidth }}
>
{title}
</div>
</div>
</TooltipContent>
</Tooltip>
);
}
+16
View File
@@ -0,0 +1,16 @@
import generate from "@/lib/og";
// Image metadata
export const size = {
width: 1222,
height: 560,
};
export const contentType = "image/png";
// Image generation
export default async function Image() {
return generate({ title: "เสือกไอดีชาวบ้าน", sub: "" });
}
export const dynamic = "force-dynamic";
+212
View File
@@ -0,0 +1,212 @@
import { TooltipTrigger } from "@radix-ui/react-tooltip";
import { eq } from "drizzle-orm";
import { BookAlert, CircleDollarSign, SendHorizonal } from "lucide-react";
import { cookies } from "next/headers";
import Image from "next/image";
import Link from "next/link";
import { Suspense } from "react";
import banner from "#/logos/artifact.webp";
import { Blocker } from "@/components/blocker";
import { ComboBox } from "@/components/combobox";
import { SimpleTooltip } from "@/components/tooltip";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
import { getArtifactConfig } from "@/lib/api";
import { db } from "@/lib/db";
import { characters, submissions } from "@/lib/db/schema";
import { PageTransition } from "../../transition";
import { CharacterChooser, ClearCookie, Disclaimer } from "./client";
import { ArtifactFormWrapper } from "./form";
import { LiveButton } from "./live";
import { RulesDialog } from "./rules";
export const metadata = {
title: "เสือกไอดีชาวบ้าน",
};
export default async function ArtifactFormPage() {
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))
: [];
const clist = await db
.select({
label: characters.name,
value: characters.name,
})
.from(characters);
const config = await getArtifactConfig();
const count = await db.$count(submissions);
return (
<PageTransition>
<div className="flex flex-col justify-around items-center h-svh">
<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={{
transform: "translateY(-70%)",
}}
className="absolute z-50"
height={137.5}
width={276.5}
src={banner}
alt="เสือกไอดีชาวบ้าน"
/>
</Link>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<ArtifactFormWrapper id="mainform">
<div className="flex flex-col gap-3">
<div className="grid gap-2">
<Label htmlFor="name">ชื่อ*</Label>
<Input
id="name"
name="name"
type="text"
placeholder="Mr.Buzz"
autoComplete="name"
maxLength={32}
required
/>
</div>
{config.enka ? (
<CharacterChooser clist={clist} />
) : (
<>
<div className="grid gap-2">
<Label htmlFor="uid">UID*</Label>
<Input
id="uid"
name="uid"
type="number"
required
placeholder="814006303"
maxLength={10}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="character">ตัวละคร*</Label>
<ComboBox
placeholder="ค้นหาตัวละคร"
id="character"
name="character"
data={clist}
className="w-full bg-transparent! hover:bg-accent!"
/>
</div>
</>
)}
<div className="grid gap-2">
<Label htmlFor="comment">ข้อความเพิ่มเติม</Label>
<Textarea
id="comment"
name="comment"
placeholder="เช่น Er พอไหมครับ, คริสวยยังครับ (ไม่บังคับ)"
className="bg-card!"
maxLength={512}
/>
</div>
</div>
</ArtifactFormWrapper>
</CardContent>
<CardFooter className="flex gap-2 justify-between">
<div className="flex gap-2">
<SimpleTooltip text="โดเนทลัดคิว ขั้นต่ำ 10 บาท">
<Link
href="https://tipme.in.th/536d969652666273c2fa85ad"
target="_blank"
rel="noreferrer"
>
<Button
variant="outline"
className={
config.limit >= 0 && count >= config.limit
? "bg-emerald-600! border-white! animate-pulse"
: ""
}
type="button"
>
<CircleDollarSign />
</Button>
</Link>
</SimpleTooltip>
<Tooltip>
<RulesDialog>
<TooltipTrigger asChild>
<Button variant="destructive" type="button">
<BookAlert />
</Button>
</TooltipTrigger>
</RulesDialog>
<TooltipContent>อ่านกฏการลงคิว</TooltipContent>
</Tooltip>
<Suspense>
<LiveButton />
</Suspense>
</div>
<div className="flex gap-2 items-center">
<span className="p-1">
{count} / {config.limit < 0 ? "∞" : config.limit}
</span>
<SimpleTooltip text="ส่งเลยจัฟลูกพี่">
<Button
type="submit"
form="mainform"
disabled={
!!q ||
config.locked ||
(config.limit >= 0 && count >= config.limit)
}
>
<SendHorizonal />
</Button>
</SimpleTooltip>
</div>
</CardFooter>
</Card>
{sid && !q && <ClearCookie />}
</div>
</PageTransition>
);
}
+45
View File
@@ -0,0 +1,45 @@
import type * as DialogPrimitive from "@radix-ui/react-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function RulesDialog({
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Root> & {
children: React.ReactNode;
}) {
return (
<Dialog {...props}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>กฏการลงคิว</DialogTitle>
</DialogHeader>
<p>
{" "}
1. คิวจะใช้ระบบเรียง หรืออาจสุ่มบางครั้ง แต่ถ้ามีโดเนทลัดคิวมาจะดูไอดีที่โดเนทมาก่อน
<br />
2. คิวจะมีการลบใหม่หมดทุกครั้งหลังจบสตรีม (ต้องลงใหม่ทุกครั้งนะคับ)
<br /> 3. สามารถดูให้ได้แค่
<u className="text-red-500">ตัวละครเดียว</u>เท่านั้น แต่ถ้าโดเนทลัดคิว
สามารถดูได้สูงสุด 8 ตัว
<br />
4. โดเนทขั้นต่ำ 10 บาท หากโดเนทมา แล้วลงคิวไปแล้ว แจ้งชื่อที่ลงไปในคิวมาด้วย
</p>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">ตกลง</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}