From a966d996a9921e900b3d01038115de87c79c45a6 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Fri, 2 Oct 2026 17:33:14 +0700 Subject: [PATCH] feat : update --- app/admin/commission/[id]/page.tsx | 4 +- app/admin/commission/page.tsx | 31 +- .../tickets/[id]/messages/route.test.ts | 68 + .../commission/tickets/[id]/messages/route.ts | 14 +- .../tickets/[id]/reactions/route.ts | 3 +- .../tickets/[id]/status/route.test.ts | 50 + .../commission/tickets/[id]/status/route.ts | 2 +- app/api/profile/route.test.ts | 63 + app/api/profile/route.ts | 59 + app/commission/page.tsx | 5 +- app/commission/tickets/[id]/page.tsx | 4 +- app/commission/tickets/page.tsx | 4 +- app/profile/page.tsx | 27 + bun.lock | 13 +- components/admin/admin-header.tsx | 27 +- components/auth/account-menu.tsx | 39 + components/auth/profile-form.tsx | 77 + .../commission/admin-availability-control.tsx | 6 +- components/commission/sign-out.tsx | 18 - components/commission/ticket-chat.tsx | 171 +- components/commission/ticket-detail.tsx | 24 +- components/commission/ticket-panels.tsx | 19 + components/commission/ticket-rename.tsx | 32 +- components/public/site-header.tsx | 2 + db/schema.ts | 10 +- drizzle/0069_useful_living_mummy.sql | 4 + drizzle/meta/0069_snapshot.json | 4002 +++++++++++++++++ drizzle/meta/_journal.json | 7 + lib/commission/reaction.test.ts | 12 + lib/commission/reaction.ts | 7 + lib/commission/tickets.ts | 12 +- lib/security/headers.ts | 4 +- next.config.ts | 2 +- package.json | 3 + 34 files changed, 4678 insertions(+), 147 deletions(-) create mode 100644 app/api/commission/tickets/[id]/messages/route.test.ts create mode 100644 app/api/commission/tickets/[id]/status/route.test.ts create mode 100644 app/api/profile/route.test.ts create mode 100644 app/api/profile/route.ts create mode 100644 app/profile/page.tsx create mode 100644 components/auth/account-menu.tsx create mode 100644 components/auth/profile-form.tsx delete mode 100644 components/commission/sign-out.tsx create mode 100644 components/commission/ticket-panels.tsx create mode 100644 drizzle/0069_useful_living_mummy.sql create mode 100644 drizzle/meta/0069_snapshot.json create mode 100644 lib/commission/reaction.test.ts create mode 100644 lib/commission/reaction.ts diff --git a/app/admin/commission/[id]/page.tsx b/app/admin/commission/[id]/page.tsx index e5aae54..844d53d 100644 --- a/app/admin/commission/[id]/page.tsx +++ b/app/admin/commission/[id]/page.tsx @@ -1,4 +1,3 @@ -import Link from "next/link"; import { notFound } from "next/navigation"; import { connection } from "next/server"; import { AdminHeader } from "@/components/admin/admin-header"; @@ -10,8 +9,7 @@ export default async function AdminCommissionTicketPage({ params }: PageProps<"/ await connection(); if (!(await getAdminSession())) notFound(); const { id } = await params; - return
- กลับไปหน้า Ticket ทั้งหมด + return
; } diff --git a/app/admin/commission/page.tsx b/app/admin/commission/page.tsx index fa4a2f3..035bcfb 100644 --- a/app/admin/commission/page.tsx +++ b/app/admin/commission/page.tsx @@ -3,12 +3,15 @@ import { desc, eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { connection } from "next/server"; import { getDb } from "@/db"; -import { commissionCheckouts, commissionTickets, users } from "@/db/schema"; +import { commissionCheckouts, commissionTickets } from "@/db/schema"; import { AdminHeader } from "@/components/admin/admin-header"; import { CommissionLiveRefresh } from "@/components/commission/live-refresh"; +import { CommissionTicketRename } from "@/components/commission/ticket-rename"; import { AdminCommissionAvailabilityControl } from "@/components/commission/admin-availability-control"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Pencil } from "lucide-react"; import { getAdminSession } from "@/lib/auth/server"; import { getCommissionAvailability } from "@/lib/commission/availability"; @@ -20,10 +23,8 @@ export default async function AdminCommissionPage({ searchParams }: PageProps<"/ const query = await searchParams; const rawPage = Number(query.page ?? 1); const page = Number.isInteger(rawPage) && rawPage > 0 ? Math.min(rawPage, 10000) : 1; - const [tickets, availability] = await Promise.all([getDb().select({ ticket: commissionTickets, amount: commissionCheckouts.amountBaht, - customer: users.name }).from(commissionTickets) + const [tickets, availability] = await Promise.all([getDb().select({ ticket: commissionTickets, amount: commissionCheckouts.amountBaht }).from(commissionTickets) .innerJoin(commissionCheckouts, eq(commissionTickets.checkoutId, commissionCheckouts.id)) - .innerJoin(users, eq(commissionTickets.userId, users.id)) .orderBy(desc(commissionTickets.status), desc(commissionTickets.updatedAt), desc(commissionTickets.id)) .limit(51).offset((page - 1) * 50), getCommissionAvailability()]); return
@@ -34,14 +35,26 @@ export default async function AdminCommissionPage({ searchParams }: PageProps<"/ {tickets.length === 0 &&

ยังไม่มี Ticket

} - {tickets.slice(0, 50).map(({ ticket, amount, customer }) => - {ticket.title || `#${ticket.id.slice(0, 8)}`} - {customer} - + {tickets.slice(0, 50).map(({ ticket, amount }) => + + + {ticket.title || `#${ticket.id.slice(0, 8)}`} + + + + {ticket.status === "open" ? "เปิดอยู่" : "ปิดแล้ว"} - + + + ฿{amount} - {ticket.updatedAt.toLocaleString("th-TH")} - )} + )} +
diff --git a/components/auth/account-menu.tsx b/components/auth/account-menu.tsx new file mode 100644 index 0000000..7965db1 --- /dev/null +++ b/components/auth/account-menu.tsx @@ -0,0 +1,39 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { LogOutIcon, UserRoundIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, + DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { authClient } from "@/lib/auth/client"; +import { unsubscribeCommissionPush } from "@/components/commission/push-client"; + +export function AccountMenu({ admin = false }: { admin?: boolean }) { + const router = useRouter(); + const { data } = authClient.useSession(); + if (!admin && !data?.user) return null; + + async function signOut() { + await unsubscribeCommissionPush().catch(() => undefined); + await authClient.signOut(); + router.push(admin ? "/admin/login" : "/commission"); + router.refresh(); + } + + return + }> + + + + + {data?.user?.name || "บัญชีของฉัน"} + }>โปรไฟล์ของฉัน + + + + void signOut()}>ออกจากระบบ + + + ; +} diff --git a/components/auth/profile-form.tsx b/components/auth/profile-form.tsx new file mode 100644 index 0000000..4e448bf --- /dev/null +++ b/components/auth/profile-form.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useEffect, useState, type FormEvent } from "react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; + +export function ProfileForm({ name: initialName, email, image: initialImage }: { + name: string; email: string; image: string | null; +}) { + const [name, setName] = useState(initialName); + const [file, setFile] = useState(null); + const [preview, setPreview] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => () => { if (preview) URL.revokeObjectURL(preview); }, [preview]); + + function chooseFile(next: File | null) { + setError(""); + if (next && (next.size > 6 * 1024 * 1024 || !["image/png", "image/jpeg", "image/webp"].includes(next.type))) { + setError("ใช้รูป PNG, JPEG หรือ WebP ขนาดไม่เกิน 6 MB"); + return; + } + setFile(next); + setPreview(next ? URL.createObjectURL(next) : null); + } + + async function save(event: FormEvent) { + event.preventDefault(); + if (busy) return; + setBusy(true); setError(""); + try { + const form = new FormData(); + form.set("name", name.trim()); + if (file) form.set("image", file); + const response = await fetch("/api/profile", { method: "POST", body: form }); + if (!response.ok) throw new Error("บันทึกโปรไฟล์ไม่สำเร็จ กรุณาลองอีกครั้ง"); + window.location.reload(); + } catch (cause) { + setError(cause instanceof Error ? cause.message : "บันทึกโปรไฟล์ไม่สำเร็จ"); + setBusy(false); + } + } + + return + ข้อมูลบัญชี{email} +
+ รูปโปรไฟล์ +
+ + {(preview || initialImage) && } + {name.trim().charAt(0).toUpperCase() || "?"} + + { + const selected = event.target.files?.[0] ?? null; + chooseFile(selected); + if (selected && (selected.size > 6 * 1024 * 1024 || + !["image/png", "image/jpeg", "image/webp"].includes(selected.type))) event.target.value = ""; + }} disabled={busy} /> +
+
+ ชื่อที่แสดง + setName(event.target.value)} + minLength={2} maxLength={80} required disabled={busy} /> + + {error && {error}} +
+ +
; +} diff --git a/components/commission/admin-availability-control.tsx b/components/commission/admin-availability-control.tsx index 1c8f427..f8615fb 100644 --- a/components/commission/admin-availability-control.tsx +++ b/components/commission/admin-availability-control.tsx @@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { setCommissionAvailability } from "@/app/admin/commission/actions"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; @@ -31,9 +30,8 @@ export function AdminCommissionAvailabilityControl({ isOpen, closureMessage }: { }); } - return
- การรับ Commission - {isOpen ? "เปิดรับ" : "ปิดรับ"}
+ return + การรับ Commission การปิดรับจะหยุดคำขอใหม่ ลูกค้าที่มีคำขออยู่แล้วชำระเงินต่อได้ diff --git a/components/commission/sign-out.tsx b/components/commission/sign-out.tsx deleted file mode 100644 index 4b728bc..0000000 --- a/components/commission/sign-out.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { LogOutIcon } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { authClient } from "@/lib/auth/client"; -import { unsubscribeCommissionPush } from "./push-client"; -import { CommissionPushEntryPrompt } from "./push-entry-prompt"; - -export function CommissionSignOut() { - const router = useRouter(); - return <>; -} diff --git a/components/commission/ticket-chat.tsx b/components/commission/ticket-chat.tsx index bf880d1..cf3d21e 100644 --- a/components/commission/ticket-chat.tsx +++ b/components/commission/ticket-chat.tsx @@ -2,8 +2,9 @@ import { useEffect, useRef, useState, type ClipboardEvent, type FormEvent, type KeyboardEvent } from "react"; import { useRouter } from "next/navigation"; -import { ImagePlus, MessageCircle, SendHorizontal, SmilePlus, Trash2, X } from "lucide-react"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import dynamic from "next/dynamic"; +import { ImagePlus, MessageCircle, ReplyIcon, SendHorizontal, SmilePlus, Trash2, X } from "lucide-react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; @@ -23,13 +24,31 @@ import { parseCommissionMessageEvent } from "@/lib/commission/message-event"; import { CommissionPushEntryPrompt } from "./push-entry-prompt"; type ChatMessage = { id: string; authorId: string; authorName: string; text: string | null; + authorImage: string | null; replyToId: string | null; imageUrl: string | null; createdAt: Date; reactions: { userId: string; emoji: string }[] }; type OptimisticMessage = { localId: string; serverId: string | null; text: string; imageUrl: string | null; - status: "sending" | "sent" }; + replyToId: string | null; status: "sending" | "sent" }; type OptimisticReaction = { active: boolean; status: "sending" | "sent" }; -const emojiChoices = ["👍", "❤️", "😂", "😮", "😢", "🎉"]; +const EmojiPicker = dynamic(() => import("emoji-picker-react"), { ssr: false }); const reactionKey = (messageId: string, emoji: string) => `${messageId}:${emoji}`; +function ReactionEmoji({ emoji }: { emoji: string }) { + const [failed, setFailed] = useState(false); + const unified = Array.from(emoji, (character) => character.codePointAt(0)!.toString(16)).join("-"); + if (failed) return ; + // CDN images are already 64 px assets displayed at 16 px. + // eslint-disable-next-line @next/next/no-img-element + return setFailed(true)} />; +} + +function ReplyPreview({ message }: { message: ChatMessage }) { + return
+ {message.authorName} +

{message.text || (message.imageUrl ? "รูปภาพ" : "ข้อความ")}

+
; +} + function CommissionMessageImage({ src }: { src: string }) { const imageRef = useRef(null); const [state, setState] = useState<"loading" | "loaded" | "error">("loading"); @@ -74,6 +93,10 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me const [optimisticMessages, setOptimisticMessages] = useState([]); const nextOptimisticId = useRef(0); const [reactionFor, setReactionFor] = useState(null); + const [replyToId, setReplyToId] = useState(null); + const [closeConfirmOpen, setCloseConfirmOpen] = useState(false); + const [closedAlertOpen, setClosedAlertOpen] = useState(false); + const [statusBusy, setStatusBusy] = useState(false); const [optimisticReactions, setOptimisticReactions] = useState>({}); const pendingReactions = useRef(new Set()); const [deleteFor, setDeleteFor] = useState(null); @@ -87,6 +110,8 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me const visibleOptimisticMessages = optimisticMessages.filter((message) => !message.serverId || !messages.some((saved) => saved.id === message.serverId)); const visibleMessages = messages.filter((message) => !deletedMessageIds.has(message.id)); + const visibleMessageById = new Map(visibleMessages.map((message) => [message.id, message])); + const replyTarget = replyToId ? visibleMessageById.get(replyToId) ?? null : null; useEffect(() => () => { if (imagePreviewRef.current) URL.revokeObjectURL(imagePreviewRef.current); @@ -125,6 +150,7 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me const receive = (event: Event) => { const value = (event as MessageEvent).data; if (!value.startsWith("typing:")) { + if (value === "status:closed" && !admin) setClosedAlertOpen(true); const message = parseCommissionMessageEvent(value); if (message && message.authorId !== userId && (document.hidden || !document.hasFocus())) { unread += 1; @@ -154,15 +180,26 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me window.removeEventListener("focus", markRead); if (document.title === managedTitle) document.title = originalTitle; }; - }, [ticketId, ticketTitle, userId, router]); + }, [ticketId, ticketTitle, userId, admin, router]); useEffect(() => { const viewport = scrollRef.current; if (viewport && stayAtBottom.current) viewport.scrollTop = viewport.scrollHeight; }, [messages, optimisticMessages]); + useEffect(() => { + const viewport = scrollRef.current; + if (!viewport) return; + const observer = new ResizeObserver(() => { + if (stayAtBottom.current) viewport.scrollTop = viewport.scrollHeight; + }); + observer.observe(viewport); + return () => observer.disconnect(); + }, []); + function sendTyping(active: boolean) { if (status !== "open") return; + // eslint-disable-next-line react-hooks/purity -- This function runs only from input event handlers. const now = Date.now(); if (active && typingActive.current && now - lastTypingSent.current < 4000) return; if (!active && !typingActive.current) return; @@ -213,15 +250,17 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me if (imageUrl) optimisticImageUrls.current.add(imageUrl); setOptimisticMessages((current) => [...current.filter((message) => !message.serverId || !messages.some((saved) => saved.id === message.serverId)), { - localId, serverId: null, text: text.trim(), imageUrl, status: "sending", + localId, serverId: null, text: text.trim(), imageUrl, replyToId: replyTarget?.id ?? null, status: "sending", }]); stayAtBottom.current = true; try { const data = new FormData(form); data.set("text", text.trim()); + if (replyTarget) data.set("replyToId", replyTarget.id); + else data.delete("replyToId"); if (image) data.set("image", image); else data.delete("image"); - form.reset(); setText(""); setAttachment(null); sendTyping(false); + form.reset(); setText(""); setAttachment(null); setReplyToId(null); sendTyping(false); const response = await fetch(`/api/commission/tickets/${ticketId}/messages`, { method: "POST", body: data }); if (!response.ok) throw new Error("ส่งข้อความไม่สำเร็จ กรุณาลองอีกครั้ง"); const result = await response.json() as { messageId: string }; @@ -232,7 +271,7 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me } catch (cause) { if (imageUrl) { URL.revokeObjectURL(imageUrl); optimisticImageUrls.current.delete(imageUrl); } setOptimisticMessages((current) => current.filter((message) => message.localId !== localId)); - setText(originalText); setAttachment(originalImage); + setText(originalText); setAttachment(originalImage); setReplyToId(replyTarget?.id ?? null); setError(cause instanceof Error ? cause.message : "ส่งข้อความไม่สำเร็จ"); sendingRef.current = false; if (deferredRefresh.current) { deferredRefresh.current = false; router.refresh(); } @@ -277,6 +316,7 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me const response = await fetch(`/api/commission/tickets/${ticketId}/messages/${messageId}`, { method: "DELETE" }); if (!response.ok) throw new Error(); setDeletedMessageIds((current) => new Set(current).add(messageId)); + if (replyToId === messageId) setReplyToId(null); setReactionFor(null); router.refresh(); } catch { setError("ลบข้อความไม่สำเร็จ กรุณาลองอีกครั้ง"); } @@ -284,12 +324,16 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me } async function toggleStatus() { + if (statusBusy) return; + setStatusBusy(true); try { const response = await fetch(`/api/commission/tickets/${ticketId}/status`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: status === "open" ? "closed" : "open" }) }); if (!response.ok) throw new Error(); + setCloseConfirmOpen(false); router.refresh(); } catch { setError("เปลี่ยนสถานะ Ticket ไม่สำเร็จ กรุณาลองอีกครั้ง"); } + finally { setStatusBusy(false); } } function handleComposerKeyDown(event: KeyboardEvent) { @@ -299,17 +343,38 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me } } - return <>{!admin && } - + return <>{!admin && } + + + Ticket นี้ปิดแล้ว + ทีมงานปิด Ticket นี้แล้ว คุณยังอ่านบทสนทนาเดิมได้ + + setClosedAlertOpen(false)}>รับทราบ + + + + แชต{admin ? "กับลูกค้า" : "กับทีมงาน"} พูดคุยและส่งรูปภาพเกี่ยวกับคำขอนี้ {status === "open" ? "เปิดอยู่" : "ปิดแล้ว"} - {admin && } + {admin && (status === "open" ? + }>ปิด Ticket + + ปิด Ticket นี้? + ลูกค้าจะได้รับแจ้งว่า Ticket ปิดแล้ว และจะส่งข้อความเพิ่มไม่ได้จนกว่าจะเปิดอีกครั้ง + + ยกเลิก + void toggleStatus()}>ยืนยันการปิด + + : )} + {!admin && status === "closed" && + Ticket นี้ปิดแล้ว คุณยังอ่านบทสนทนาเดิมได้ + } { const viewport = event.currentTarget; stayAtBottom.current = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 80; }} @@ -319,11 +384,17 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me เริ่มบทสนทนา ส่งข้อความหรือรูปภาพเพื่อพูดคุยเกี่ยวกับคำขอนี้ - : + : {visibleMessages.map((message) => { const mine = message.authorId === userId; const sentAt = new Date(message.createdAt); - const counts = emojiChoices.map((emoji) => { + const parent = message.replyToId ? visibleMessageById.get(message.replyToId) : null; + const emojiValues = new Set([ + ...message.reactions.map((reaction) => reaction.emoji), + ...Object.keys(optimisticReactions).filter((key) => key.startsWith(`${message.id}:`)) + .map((key) => key.slice(message.id.length + 1)), + ]); + const counts = [...emojiValues].map((emoji) => { const savedCount = message.reactions.filter((reaction) => reaction.emoji === emoji).length; const savedActive = message.reactions.some((reaction) => reaction.emoji === emoji && reaction.userId === userId); const update = optimisticReactions[reactionKey(message.id, emoji)]; @@ -333,42 +404,35 @@ export function CommissionTicketChat({ ticketId, ticketTitle, userId, status, me return { emoji, count, active, pending: update?.status === "sending" }; }).filter(({ count }) => count > 0); return - {!mine && + + {message.authorImage && } + {message.authorName.trim().charAt(0).toUpperCase() || "?"} - } - + + {mine ? "คุณ" : message.authorName} - +
+ + {parent && } {message.text &&

{message.text}

} {message.imageUrl && }
- {counts.length > 0 &&
- {counts.map(({ emoji, count, active, pending }) => )} -
} - - +
{status === "open" && setReactionFor(open ? message.id : null)}> }> - -
- {emojiChoices.map((emoji) => )} -
+ + void react(message.id, value.emoji)} />
} + {status === "open" && } {mine && setDeleteFor(open ? message.id : null)}> } +
+
+ {counts.length > 0 &&
+ {counts.map(({ emoji, count, active, pending }) => )} +
} + +
; })} {visibleOptimisticMessages.map((message) => - + คุณ - + + {message.replyToId && visibleMessageById.get(message.replyToId) && + } {message.text &&

{message.text}

} {message.imageUrl &&
}
- {message.status === "sending" ? "กำลังส่ง..." : "ส่งแล้ว"} + + {message.status === "sending" ? "กำลังส่ง..." : "ส่งแล้ว"} +
)}
}
- + {typing && status === "open" &&

{typing.name} typing...

} {status === "open" ?
+ {replyTarget &&
+
+ +
} {image &&
{/* eslint-disable-next-line @next/next/no-img-element */} {imagePreview && ตัวอย่างรูปภาพที่จะแนบ} diff --git a/components/commission/ticket-detail.tsx b/components/commission/ticket-detail.tsx index b47468e..5fa99b8 100644 --- a/components/commission/ticket-detail.tsx +++ b/components/commission/ticket-detail.tsx @@ -5,8 +5,8 @@ import { publicMediaUrl } from "@/lib/media/storage"; import { HttpError } from "@/lib/security/http"; import { CommissionRequestSummary } from "./request-summary"; import { CommissionTicketChat } from "./ticket-chat"; -import { CommissionTicketRename } from "./ticket-rename"; import { CommissionSlipDialog } from "./slip-dialog"; +import { CommissionTicketPanels } from "./ticket-panels"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; export async function CommissionTicketDetail({ id, admin }: { id: string; admin: boolean }) { @@ -19,25 +19,17 @@ export async function CommissionTicketDetail({ id, admin }: { id: string; admin: throw cause; } const catalog = await getCommissionLabels(data.checkout.request); - return
-
-

- {data.ticket.title || `Ticket #${data.ticket.id.slice(0, 8)}`} -

-

{data.ticket.title ? `#${data.ticket.id.slice(0, 8)} - ` : ""}{data.customerName} - {data.ticket.status === "open" ? "เปิด" : "ปิด"}

-
- {admin && } -
-
- คำขอที่ชำระเงินแล้ว - ฿{data.checkout.amountBaht} - {catalog && } + return
+ คำขอที่ชำระเงินแล้ว - ฿{data.checkout.amountBaht} + {catalog && }

การชำระเงิน: {data.payment.transRef} - {data.payment.transferredAt.toLocaleString("th-TH")}

- ({ ...message, - imageUrl: imageObjectKey ? publicMediaUrl(imageObjectKey) : null }))} admin={admin} /> -
+ imageUrl: imageObjectKey ? publicMediaUrl(imageObjectKey) : null }))} admin={admin} />} + />
; } diff --git a/components/commission/ticket-panels.tsx b/components/commission/ticket-panels.tsx new file mode 100644 index 0000000..ac875ea --- /dev/null +++ b/components/commission/ticket-panels.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { useState, type ReactNode } from "react"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { cn } from "@/lib/utils"; + +export function CommissionTicketPanels({ summary, chat }: { summary: ReactNode; chat: ReactNode }) { + const [active, setActive] = useState<"chat" | "summary">("chat"); + return
+ { if (values[0] === "chat" || values[0] === "summary") setActive(values[0]); }} + className="shrink-0 lg:hidden"> + แชต + รายละเอียดคำขอ + +
{summary}
+
{chat}
+
; +} diff --git a/components/commission/ticket-rename.tsx b/components/commission/ticket-rename.tsx index 7ebcf5a..b81dd41 100644 --- a/components/commission/ticket-rename.tsx +++ b/components/commission/ticket-rename.tsx @@ -1,29 +1,38 @@ "use client"; -import { useState, type FormEvent } from "react"; +import { useState, type FormEvent, type MouseEvent, type ReactNode } from "react"; import { useRouter } from "next/navigation"; -import { Pencil } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -export function CommissionTicketRename({ ticketId, title }: { ticketId: string; title: string | null }) { +export function CommissionTicketRename({ children }: { children: ReactNode }) { const router = useRouter(); const [open, setOpen] = useState(false); - const [value, setValue] = useState(title ?? ""); + const [ticketId, setTicketId] = useState(null); + const [value, setValue] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); function changeOpen(next: boolean) { if (busy) return; setOpen(next); - if (next) { setValue(title ?? ""); setError(""); } + } + + function openFromClick(event: MouseEvent) { + if (!(event.target instanceof Element)) return; + const trigger = event.target.closest("[data-ticket-rename-id]"); + if (!trigger) return; + setTicketId(trigger.dataset.ticketRenameId ?? null); + setValue(trigger.dataset.ticketRenameTitle ?? ""); + setError(""); + setOpen(true); } async function save(event: FormEvent) { event.preventDefault(); - if (busy) return; + if (busy || !ticketId) return; setBusy(true); setError(""); try { const response = await fetch(`/api/commission/tickets/${ticketId}/title`, { @@ -36,10 +45,8 @@ export function CommissionTicketRename({ ticketId, title }: { ticketId: string; finally { setBusy(false); } } - return - }> - เปลี่ยนชื่อ - + return
{children} +
@@ -58,5 +65,6 @@ export function CommissionTicketRename({ ticketId, title }: { ticketId: string;
-
; +
+
; } diff --git a/components/public/site-header.tsx b/components/public/site-header.tsx index af63c7f..945e1f0 100644 --- a/components/public/site-header.tsx +++ b/components/public/site-header.tsx @@ -5,6 +5,7 @@ import { PencilIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { VisitorPresence } from "@/components/public/visitor-presence"; +import { AccountMenu } from "@/components/auth/account-menu"; import { getAdminSession } from "@/lib/auth/server"; async function AdminEditLink({ editHref }: { editHref: string }) { @@ -52,6 +53,7 @@ export function SiteHeader({ editHref = "/admin", guideId }: { editHref?: string
+
); diff --git a/db/schema.ts b/db/schema.ts index f109fe0..8fa02e4 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -1,6 +1,7 @@ import { bigint, boolean, + foreignKey, index, integer, jsonb, @@ -214,16 +215,21 @@ export const commissionMessages = commissionSchema.table("message", { id: uuid("id").defaultRandom().primaryKey(), ticketId: uuid("ticket_id").notNull().references(() => commissionTickets.id), authorId: text("author_id").notNull().references(() => users.id), + replyToId: uuid("reply_to_id"), text: text("text"), imageObjectKey: text("image_object_key"), imageMimeType: varchar("image_mime_type", { length: 32 }), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), -}, (table) => [index("commission_message_ticket_time_idx").on(table.ticketId, table.createdAt)]); +}, (table) => [ + index("commission_message_ticket_time_idx").on(table.ticketId, table.createdAt), + index("commission_message_reply_idx").on(table.replyToId), + foreignKey({ columns: [table.replyToId], foreignColumns: [table.id] }).onDelete("set null"), +]); export const commissionReactions = commissionSchema.table("reaction", { messageId: uuid("message_id").notNull().references(() => commissionMessages.id, { onDelete: "cascade" }), userId: text("user_id").notNull().references(() => users.id), - emoji: varchar("emoji", { length: 16 }).notNull(), + emoji: text("emoji").notNull(), }, (table) => [primaryKey({ columns: [table.messageId, table.userId, table.emoji] })]); diff --git a/drizzle/0069_useful_living_mummy.sql b/drizzle/0069_useful_living_mummy.sql new file mode 100644 index 0000000..e5aeb89 --- /dev/null +++ b/drizzle/0069_useful_living_mummy.sql @@ -0,0 +1,4 @@ +ALTER TABLE "commission"."reaction" ALTER COLUMN "emoji" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "commission"."message" ADD COLUMN "reply_to_id" uuid;--> statement-breakpoint +ALTER TABLE "commission"."message" ADD CONSTRAINT "message_reply_to_id_message_id_fk" FOREIGN KEY ("reply_to_id") REFERENCES "commission"."message"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "commission_message_reply_idx" ON "commission"."message" USING btree ("reply_to_id"); \ No newline at end of file diff --git a/drizzle/meta/0069_snapshot.json b/drizzle/meta/0069_snapshot.json new file mode 100644 index 0000000..98f00d2 --- /dev/null +++ b/drizzle/meta/0069_snapshot.json @@ -0,0 +1,4002 @@ +{ + "id": "300a0d02-bdb6-414d-8019-6690c70e4029", + "prevId": "d800c009-6052-46b1-a739-38c974362f76", + "version": "7", + "dialect": "postgresql", + "tables": { + "auth.account": { + "name": "account", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_provider_unique": { + "name": "account_provider_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_condition": { + "name": "artifact_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_condition_order_idx": { + "name": "artifact_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_condition_guide_id_guide_id_fk": { + "name": "artifact_condition_guide_id_guide_id_fk", + "tableFrom": "artifact_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_group": { + "name": "artifact_group", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_group_order_idx": { + "name": "artifact_group_order_idx", + "columns": [ + { + "expression": "condition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_group_condition_id_artifact_condition_id_fk": { + "name": "artifact_group_condition_id_artifact_condition_id_fk", + "tableFrom": "artifact_group", + "tableTo": "artifact_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_profile": { + "name": "artifact_profile", + "schema": "guides", + "columns": { + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "sands": { + "name": "sands", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "goblet": { + "name": "goblet", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "circlet": { + "name": "circlet", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "substats": { + "name": "substats", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_conditions": { + "name": "recommended_conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "targets": { + "name": "targets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "target_conditions": { + "name": "target_conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "talents": { + "name": "talents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "artifact_profile_guide_id_guide_id_fk": { + "name": "artifact_profile_guide_id_guide_id_fk", + "tableFrom": "artifact_profile", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_recommendation": { + "name": "artifact_recommendation", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "first_artifact_key": { + "name": "first_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "second_artifact_key": { + "name": "second_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "percent": { + "name": "percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_recommendation_order_idx": { + "name": "artifact_recommendation_order_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_recommendation_group_id_artifact_group_id_fk": { + "name": "artifact_recommendation_group_id_artifact_group_id_fk", + "tableFrom": "artifact_recommendation", + "tableTo": "artifact_group", + "schemaTo": "guides", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "audit_log_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_time_idx": { + "name": "audit_log_time_idx", + "columns": [ + { + "expression": "time", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_author_time_idx": { + "name": "audit_log_author_time_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.artifact": { + "name": "artifact", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_artifact_name_idx": { + "name": "catalog_artifact_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "artifact_key_pk": { + "name": "artifact_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.character": { + "name": "character", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element": { + "name": "element", + "type": "varchar(48)", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weapon_type": { + "name": "weapon_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element_icon_key": { + "name": "element_icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coop_image_key": { + "name": "coop_image_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_card_image_key": { + "name": "name_card_image_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constellations": { + "name": "constellations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "talents": { + "name": "talents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "ascension": { + "name": "ascension", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "leveling": { + "name": "leveling", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "base_stats": { + "name": "base_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "catalog_character_name_idx": { + "name": "catalog_character_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "character_key_pk": { + "name": "character_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.material": { + "name": "material", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "catalog_material_name_idx": { + "name": "catalog_material_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.weapon": { + "name": "weapon", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weapon_type": { + "name": "weapon_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_weapon_name_idx": { + "name": "catalog_weapon_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "weapon_key_pk": { + "name": "weapon_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.checkout": { + "name": "checkout", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "amount_baht": { + "name": "amount_baht", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_checkout_user_idx": { + "name": "commission_checkout_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "checkout_user_id_user_id_fk": { + "name": "checkout_user_id_user_id_fk", + "tableFrom": "checkout", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.message": { + "name": "message", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ticket_id": { + "name": "ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_object_key": { + "name": "image_object_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_mime_type": { + "name": "image_mime_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_message_ticket_time_idx": { + "name": "commission_message_ticket_time_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_message_reply_idx": { + "name": "commission_message_reply_idx", + "columns": [ + { + "expression": "reply_to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_ticket_id_ticket_id_fk": { + "name": "message_ticket_id_ticket_id_fk", + "tableFrom": "message", + "tableTo": "ticket", + "schemaTo": "commission", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_author_id_user_id_fk": { + "name": "message_author_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_reply_to_id_message_id_fk": { + "name": "message_reply_to_id_message_id_fk", + "tableFrom": "message", + "tableTo": "message", + "schemaTo": "commission", + "columnsFrom": [ + "reply_to_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.payment": { + "name": "payment", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkout_id": { + "name": "checkout_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trans_ref": { + "name": "trans_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_baht": { + "name": "amount_baht", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transferred_at": { + "name": "transferred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "slip_object_key": { + "name": "slip_object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slip_mime_type": { + "name": "slip_mime_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "provider_data": { + "name": "provider_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_payment_checkout_unique": { + "name": "commission_payment_checkout_unique", + "columns": [ + { + "expression": "checkout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_payment_reference_unique": { + "name": "commission_payment_reference_unique", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_payment_trans_ref_unique": { + "name": "commission_payment_trans_ref_unique", + "columns": [ + { + "expression": "trans_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_checkout_id_checkout_id_fk": { + "name": "payment_checkout_id_checkout_id_fk", + "tableFrom": "payment", + "tableTo": "checkout", + "schemaTo": "commission", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payment_ticket_id_ticket_id_fk": { + "name": "payment_ticket_id_ticket_id_fk", + "tableFrom": "payment", + "tableTo": "ticket", + "schemaTo": "commission", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.push_subscription": { + "name": "push_subscription", + "schema": "commission", + "columns": { + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_push_subscription_user_idx": { + "name": "commission_push_subscription_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.reaction": { + "name": "reaction", + "schema": "commission", + "columns": { + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reaction_message_id_message_id_fk": { + "name": "reaction_message_id_message_id_fk", + "tableFrom": "reaction", + "tableTo": "message", + "schemaTo": "commission", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reaction_user_id_user_id_fk": { + "name": "reaction_user_id_user_id_fk", + "tableFrom": "reaction", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reaction_message_id_user_id_emoji_pk": { + "name": "reaction_message_id_user_id_emoji_pk", + "columns": [ + "message_id", + "user_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.settings": { + "name": "settings", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "is_open": { + "name": "is_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "closure_message": { + "name": "closure_message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.ticket": { + "name": "ticket", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkout_id": { + "name": "checkout_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_ticket_checkout_unique": { + "name": "commission_ticket_checkout_unique", + "columns": [ + { + "expression": "checkout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_ticket_user_idx": { + "name": "commission_ticket_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_ticket_status_idx": { + "name": "commission_ticket_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ticket_checkout_id_checkout_id_fk": { + "name": "ticket_checkout_id_checkout_id_fk", + "tableFrom": "ticket", + "tableTo": "checkout", + "schemaTo": "commission", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ticket_user_id_user_id_fk": { + "name": "ticket_user_id_user_id_fk", + "tableFrom": "ticket", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.constellation_condition": { + "name": "constellation_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "constellation_condition_order_idx": { + "name": "constellation_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "constellation_condition_guide_id_guide_id_fk": { + "name": "constellation_condition_guide_id_guide_id_fk", + "tableFrom": "constellation_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.constellation_recommendation": { + "name": "constellation_recommendation", + "schema": "guides", + "columns": { + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cumulative_percent": { + "name": "cumulative_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "is_great": { + "name": "is_great", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "constellation_recommendation_condition_id_constellation_condition_id_fk": { + "name": "constellation_recommendation_condition_id_constellation_condition_id_fk", + "tableFrom": "constellation_recommendation", + "tableTo": "constellation_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "constellation_recommendation_condition_id_level_pk": { + "name": "constellation_recommendation_condition_id_level_pk", + "columns": [ + "condition_id", + "level" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.extra_section_row": { + "name": "extra_section_row", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heading": { + "name": "heading", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "percent": { + "name": "percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "background_color": { + "name": "background_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "text_color": { + "name": "text_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "highlights": { + "name": "highlights", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "catalog_items": { + "name": "catalog_items", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sheet": { + "name": "sheet", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "extra_section_row_order_idx": { + "name": "extra_section_row_order_idx", + "columns": [ + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "extra_section_row_section_id_extra_section_section_id_fk": { + "name": "extra_section_row_section_id_extra_section_section_id_fk", + "tableFrom": "extra_section_row", + "tableTo": "extra_section", + "schemaTo": "guides", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "section_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "extra_section_row_media_id_media_id_fk": { + "name": "extra_section_row_media_id_media_id_fk", + "tableFrom": "extra_section_row", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.extra_section": { + "name": "extra_section", + "schema": "guides", + "columns": { + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "extra_section_section_id_guide_section_id_fk": { + "name": "extra_section_section_id_guide_section_id_fk", + "tableFrom": "extra_section", + "tableTo": "guide_section", + "schemaTo": "guides", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.glossary_alias": { + "name": "glossary_alias", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "catalog_key": { + "name": "catalog_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "normalized_alias": { + "name": "normalized_alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "glossary_alias_item_normalized_unique": { + "name": "glossary_alias_item_normalized_unique", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_alias", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "glossary_alias_item_idx": { + "name": "glossary_alias_item_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide_poster_bleeding": { + "name": "guide_poster_bleeding", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "scale": { + "name": "scale", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "blend_mode": { + "name": "blend_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'multiply'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "guide_poster_bleeding_order_idx": { + "name": "guide_poster_bleeding_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_poster_bleeding_guide_id_guide_id_fk": { + "name": "guide_poster_bleeding_guide_id_guide_id_fk", + "tableFrom": "guide_poster_bleeding", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "guide_poster_bleeding_media_id_media_id_fk": { + "name": "guide_poster_bleeding_media_id_media_id_fk", + "tableFrom": "guide_poster_bleeding", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide_section": { + "name": "guide_section", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "guide_section_slug_unique": { + "name": "guide_section_slug_unique", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_section_order_idx": { + "name": "guide_section_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_section_guide_id_guide_id_fk": { + "name": "guide_section_guide_id_guide_id_fk", + "tableFrom": "guide_section", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide": { + "name": "guide", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_key": { + "name": "character_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overview": { + "name": "overview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "youtube_urls": { + "name": "youtube_urls", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cover_media_id": { + "name": "cover_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "poster_art_media_id": { + "name": "poster_art_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "poster_art_offset_x": { + "name": "poster_art_offset_x", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_art_offset_y": { + "name": "poster_art_offset_y", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_art_scale": { + "name": "poster_art_scale", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "poster_art_rotation": { + "name": "poster_art_rotation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_top_colors": { + "name": "poster_top_colors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"#000000\",\"#000000\"]'::jsonb" + }, + "poster_bottom_colors": { + "name": "poster_bottom_colors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"#000000\",\"#000000\"]'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "trashed_at": { + "name": "trashed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "guide_character_key_unique": { + "name": "guide_character_key_unique", + "columns": [ + { + "expression": "character_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_slug_unique": { + "name": "guide_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_directory_idx": { + "name": "guide_directory_idx", + "columns": [ + { + "expression": "trashed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_cover_media_id_media_id_fk": { + "name": "guide_cover_media_id_media_id_fk", + "tableFrom": "guide", + "tableTo": "media", + "columnsFrom": [ + "cover_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "guide_poster_art_media_id_media_id_fk": { + "name": "guide_poster_art_media_id_media_id_fk", + "tableFrom": "guide", + "tableTo": "media", + "columnsFrom": [ + "poster_art_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "current_reference_count": { + "name": "current_reference_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_object_key_unique": { + "name": "media_object_key_unique", + "columns": [ + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_visibility_idx": { + "name": "media_visibility_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_reference_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_created_by_id_user_id_fk": { + "name": "media_created_by_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "outbox_event_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "topic": { + "name": "topic", + "type": "varchar(160)", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_pending_idx": { + "name": "outbox_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.session": { + "name": "session", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_boss": { + "name": "stygian_boss", + "schema": "guides", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hp": { + "name": "hp", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mechanics": { + "name": "mechanics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_mechanics": { + "name": "recommended_mechanics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_mechanic_types": { + "name": "recommended_mechanic_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "stygian_boss_schedule_idx": { + "name": "stygian_boss_schedule_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stygian_boss_schedule_id_stygian_schedule_schedule_id_fk": { + "name": "stygian_boss_schedule_id_stygian_schedule_schedule_id_fk", + "tableFrom": "stygian_boss", + "tableTo": "stygian_schedule", + "schemaTo": "guides", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "schedule_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "stygian_boss_schedule_id_level_position_pk": { + "name": "stygian_boss_schedule_id_level_position_pk", + "columns": [ + "schedule_id", + "level", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_recommendation_group": { + "name": "stygian_recommendation_group", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "boss_position": { + "name": "boss_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "stygian_recommendation_group_order_idx": { + "name": "stygian_recommendation_group_order_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "boss_position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stygian_recommendation_group_schedule_id_stygian_schedule_schedule_id_fk": { + "name": "stygian_recommendation_group_schedule_id_stygian_schedule_schedule_id_fk", + "tableFrom": "stygian_recommendation_group", + "tableTo": "stygian_schedule", + "schemaTo": "guides", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "schedule_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_recommendation_item": { + "name": "stygian_recommendation_item", + "schema": "guides", + "columns": { + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true + }, + "catalog_key": { + "name": "catalog_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "refinement": { + "name": "refinement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "constellation_level": { + "name": "constellation_level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "element": { + "name": "element", + "type": "varchar(24)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stygian_recommendation_item_group_id_stygian_recommendation_group_id_fk": { + "name": "stygian_recommendation_item_group_id_stygian_recommendation_group_id_fk", + "tableFrom": "stygian_recommendation_item", + "tableTo": "stygian_recommendation_group", + "schemaTo": "guides", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "stygian_recommendation_item_group_id_position_pk": { + "name": "stygian_recommendation_item_group_id_position_pk", + "columns": [ + "group_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_schedule": { + "name": "stygian_schedule", + "schema": "guides", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "challenge_name": { + "name": "challenge_name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "schedule_start_time": { + "name": "schedule_start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "schedule_end_time": { + "name": "schedule_end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_composition": { + "name": "team_composition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotation_seconds": { + "name": "rotation_seconds", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "combo": { + "name": "combo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "team_composition_order_idx": { + "name": "team_composition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_composition_guide_id_guide_id_fk": { + "name": "team_composition_guide_id_guide_id_fk", + "tableFrom": "team_composition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_composition_condition_id_team_condition_id_fk": { + "name": "team_composition_condition_id_team_condition_id_fk", + "tableFrom": "team_composition", + "tableTo": "team_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_condition": { + "name": "team_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "team_condition_order_idx": { + "name": "team_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_condition_guide_id_guide_id_fk": { + "name": "team_condition_guide_id_guide_id_fk", + "tableFrom": "team_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_member": { + "name": "team_member", + "schema": "guides", + "columns": { + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "character_key": { + "name": "character_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "artifact_key": { + "name": "artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "second_artifact_key": { + "name": "second_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "constellation": { + "name": "constellation", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "refinement": { + "name": "refinement", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "damage": { + "name": "damage", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_member_team_id_team_composition_id_fk": { + "name": "team_member_team_id_team_composition_id_fk", + "tableFrom": "team_member", + "tableTo": "team_composition", + "schemaTo": "guides", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_member_team_id_position_pk": { + "name": "team_member_team_id_position_pk", + "columns": [ + "team_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user": { + "name": "user", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verification": { + "name": "verification", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.weapon_condition": { + "name": "weapon_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "weapon_condition_order_idx": { + "name": "weapon_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weapon_condition_guide_id_guide_id_fk": { + "name": "weapon_condition_guide_id_guide_id_fk", + "tableFrom": "weapon_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.weapon_recommendation": { + "name": "weapon_recommendation", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "refinement": { + "name": "refinement", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "overall_percent": { + "name": "overall_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "personal_percent": { + "name": "personal_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "weapon_recommendation_order_idx": { + "name": "weapon_recommendation_order_idx", + "columns": [ + { + "expression": "condition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weapon_recommendation_condition_id_weapon_condition_id_fk": { + "name": "weapon_recommendation_condition_id_weapon_condition_id_fk", + "tableFrom": "weapon_recommendation", + "tableTo": "weapon_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 5501544..47ebb3f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -484,6 +484,13 @@ "when": 1790915901646, "tag": "0068_concerned_union_jack", "breakpoints": true + }, + { + "idx": 69, + "version": "7", + "when": 1790933217844, + "tag": "0069_useful_living_mummy", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/commission/reaction.test.ts b/lib/commission/reaction.test.ts new file mode 100644 index 0000000..1e5fd65 --- /dev/null +++ b/lib/commission/reaction.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { isSingleEmoji } from "./reaction"; + +describe("commission reactions", () => { + it("accepts emoji outside the old six choices, including sequences", () => { + for (const emoji of ["🔥", "🇹🇭", "👨‍👩‍👧‍👦", "1️⃣"]) expect(isSingleEmoji(emoji)).toBe(true); + }); + + it("rejects text and multiple reactions", () => { + for (const value of ["", "hello", "1", "🔥🔥", "🔥 hello"]) expect(isSingleEmoji(value)).toBe(false); + }); +}); diff --git a/lib/commission/reaction.ts b/lib/commission/reaction.ts new file mode 100644 index 0000000..4ec7d2b --- /dev/null +++ b/lib/commission/reaction.ts @@ -0,0 +1,7 @@ +import emojiRegex from "emoji-regex"; + +export function isSingleEmoji(value: string): boolean { + if (!value || value.length > 64) return false; + const matches = [...value.matchAll(emojiRegex())]; + return matches.length === 1 && matches[0][0] === value; +} diff --git a/lib/commission/tickets.ts b/lib/commission/tickets.ts index bea8637..8938785 100644 --- a/lib/commission/tickets.ts +++ b/lib/commission/tickets.ts @@ -18,7 +18,7 @@ export async function getCommissionTicket(id: string) { if (!row || (user.role !== "admin" || !user.emailVerified) && row.ticket.userId !== user.id) throw new HttpError(404, "ticket-not-found"); const [messages, reactions] = await Promise.all([ - getDb().select({ message: commissionMessages, authorName: users.name }).from(commissionMessages) + getDb().select({ message: commissionMessages, authorName: users.name, authorImage: users.image }).from(commissionMessages) .innerJoin(users, eq(commissionMessages.authorId, users.id)) .where(eq(commissionMessages.ticketId, id)).orderBy(asc(commissionMessages.createdAt)), getDb().select({ reaction: commissionReactions, messageId: commissionMessages.id }) @@ -26,8 +26,14 @@ export async function getCommissionTicket(id: string) { .innerJoin(commissionMessages, eq(commissionReactions.messageId, commissionMessages.id)) .where(eq(commissionMessages.ticketId, id)), ]); - return { ...row, messages: messages.map(({ message, authorName }) => ({ ...message, authorName, - reactions: reactions.filter(({ messageId }) => messageId === message.id).map(({ reaction }) => reaction) })), + const reactionsByMessage = new Map(); + for (const { reaction, messageId } of reactions) { + const group = reactionsByMessage.get(messageId) ?? []; + group.push(reaction); + reactionsByMessage.set(messageId, group); + } + return { ...row, messages: messages.map(({ message, authorName, authorImage }) => ({ ...message, authorName, + authorImage, reactions: reactionsByMessage.get(message.id) ?? [] })), currentUserId: user.id }; } diff --git a/lib/security/headers.ts b/lib/security/headers.ts index 7f170ff..c29d95c 100644 --- a/lib/security/headers.ts +++ b/lib/security/headers.ts @@ -1,4 +1,4 @@ -export function securityHeaders(production: boolean) { +export function securityHeaders(production: boolean, emojiImageCdn: string) { const ads = "https://*.googlesyndication.com https://*.doubleclick.net https://*.googleadservices.com https://*.googletagservices.com https://www.google.com"; const cloudflareAnalytics = "https://static.cloudflareinsights.com"; const adConnections = "https://*.adtrafficquality.google"; @@ -6,7 +6,7 @@ export function securityHeaders(production: boolean) { "default-src 'self'", `script-src 'self' 'unsafe-inline' ${production ? "" : "'unsafe-eval'"} https://challenges.cloudflare.com ${cloudflareAnalytics} ${ads}`, "style-src 'self' 'unsafe-inline'", - `img-src 'self' data: blob: https://buzz-cdn.astrxl.dev ${ads}`, + `img-src 'self' data: blob: https://buzz-cdn.astrxl.dev ${emojiImageCdn} ${ads}`, "font-src 'self' data:", `connect-src 'self' https://buzz-cdn.astrxl.dev https://challenges.cloudflare.com ${adConnections} ${ads}${production ? "" : " ws: wss:"}`, `frame-src https://challenges.cloudflare.com https://www.youtube-nocookie.com ${ads}`, diff --git a/next.config.ts b/next.config.ts index cff6142..0b0bc95 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,7 +5,7 @@ import { securityHeaders } from "./lib/security/headers"; const nextConfig: NextConfig = { poweredByHeader: false, async headers() { - return [{ source: "/:path*", headers: securityHeaders(process.env.NODE_ENV === "production") }]; + return [{ source: "/:path*", headers: securityHeaders(process.env.NODE_ENV === "production", "https://cdn.jsdelivr.net") }]; }, cacheComponents: true, cacheHandlers: { diff --git a/package.json b/package.json index 7fc7da0..68134f7 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "discord.js": "^14.27.0", "drizzle-orm": "^0.45.2", "embla-carousel-react": "^8.6.0", + "emoji-picker-react": "^4.22.3", + "emoji-regex": "^11.0.0", "entities": "7.0.1", "ioredis": "^6.0.0", "lucide-react": "^1.35.0", @@ -70,6 +72,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/bun": "^1.4.0", + "@types/emoji-regex": "^9.2.2", "@types/node": "^20", "@types/qrcode": "^1.5.6", "@types/react": "^19",