"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Image from "@/components/ui/resilient-image"; import { UserRoundIcon, XIcon } from "lucide-react"; import { ArtifactSelect, CharacterSelect, WeaponSelect } from "@/components/admin/ui/catalog-select"; import { ConstellationSelect, RefinementSelect } from "@/components/admin/ui/option-select"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { FieldError, FieldGroup, FieldLegend, FieldSet } from "@/components/ui/field"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { commissionPrice, type CommissionRequest } from "@/lib/commission/request"; import { commissionDraftKey, restoreCommissionDraft, serializeCommissionDraft } from "@/lib/commission/draft"; import { commissionRequestIssues } from "@/lib/commission/request-validation"; import { elementBackgroundColor } from "@/lib/catalog/element"; type Item = { key: string; name: string; aliases: string[]; imageUrl: string; rarity?: number | null; element?: string | null; weaponType?: string | null }; type Catalog = { characters: Item[]; weapons: Item[]; artifacts: Item[] }; const blankMember = (): CommissionRequest["teams"][number]["members"][number] => ({ characterKey: "", weaponKey: "", artifactKey: "", constellation: "0", refinement: "1", }); const blankTeam = () => ({ members: Array.from({ length: 4 }, blankMember) }); function checkoutError(code: string | undefined, status: number): string { if (status === 401) return "เซสชันหมดอายุ กรุณาเข้าสู่ระบบอีกครั้ง"; if (status === 403) return "ไม่สามารถส่งคำขอจากหน้านี้ได้ กรุณาโหลดหน้าใหม่แล้วลองอีกครั้ง"; if (status === 429) return "ส่งคำขอบ่อยเกินไป กรุณารอสักครู่แล้วลองอีกครั้ง"; if (code === "invalid-promptpay-value" || code === "invalid-promptpay-type" || code === "COMMISSION_PROMPTPAY_VALUE-not-configured") return "ระบบยังตั้งค่าบัญชี PromptPay ไม่ถูกต้อง กรุณาแจ้งผู้ดูแลระบบ"; if (code === "invalid-commission-request") return "รายการที่เลือกไม่ตรงกับข้อมูลล่าสุด กรุณาโหลดหน้าใหม่แล้วเลือกอีกครั้ง"; if (code === "commission-closed") return "ขณะนี้ปิดรับ Commission ใหม่ กรุณาโหลดหน้าเพื่อดูประกาศ"; return "สร้างคำขอไม่สำเร็จ กรุณาลองอีกครั้ง"; } export function CommissionRequestForm({ catalog, userId }: { catalog: Catalog; userId: string }) { const router = useRouter(); const [request, setRequest] = useState({ teams: [blankTeam()], weapons: [], constellations: [] }); const root = useRef(null); const restoredKey = useRef(null); const submitted = useRef(false); const submitting = useRef(false); const [hydrated, setHydrated] = useState(false); const [draftNotice, setDraftNotice] = useState(""); const [attempted, setAttempted] = useState(false); const storageKey = commissionDraftKey(userId); const issues = attempted ? commissionRequestIssues(request) : []; const invalid = (path: string) => issues.some((issue) => issue.path === path || path.startsWith(`${issue.path}.`)); const errorsAt = (path: string) => [...new Set(issues.filter((issue) => issue.path === path || issue.path.startsWith(`${path}.`)).map((issue) => issue.message))]; useEffect(() => { if (restoredKey.current === storageKey) return; let restored: CommissionRequest | null = null; let notice = "แบบร่างจะบันทึกในอุปกรณ์นี้อัตโนมัติ"; try { restored = restoreCommissionDraft(localStorage.getItem(storageKey), catalog); if (restored) notice = "กู้คืนแบบร่างแล้ว กรุณาตรวจสอบรายการก่อนชำระเงิน"; } catch { notice = "เบราว์เซอร์ไม่อนุญาตให้บันทึกแบบร่าง กรุณาเปิดหน้านี้ไว้จนสร้างคำขอสำเร็จ"; } const frame = requestAnimationFrame(() => { if (restored) setRequest(restored); restoredKey.current = storageKey; setDraftNotice(notice); setHydrated(true); }); return () => cancelAnimationFrame(frame); }, [storageKey, catalog]); useEffect(() => { if (!hydrated || submitted.current) return; try { localStorage.setItem(storageKey, serializeCommissionDraft(request)); } catch { const frame = requestAnimationFrame(() => setDraftNotice("บันทึกแบบร่างไม่สำเร็จ กรุณาเปิดหน้านี้ไว้จนสร้างคำขอสำเร็จ")); return () => cancelAnimationFrame(frame); } }, [request, hydrated, storageKey]); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const patch = (next: Partial) => setRequest((old) => ({ ...old, ...next })); const compatibleWeapons = (characterKey: string) => { const type = catalog.characters.find((item) => item.key === characterKey)?.weaponType; return catalog.weapons.filter((item) => type && item.weaponType === type); }; async function submit() { if (!hydrated || submitting.current || submitted.current) return; setAttempted(true); const validationIssues = commissionRequestIssues(request); if (validationIssues.length) { setError(""); requestAnimationFrame(() => { const field = root.current?.querySelector('[aria-invalid="true"], [data-commission-errors]'); field?.scrollIntoView({ block: "center", behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" }); field?.focus({ preventScroll: true }); }); return; } submitting.current = true; setBusy(true); setError(""); try { const response = await fetch("/api/commission/checkouts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request) }); if (!response.ok) { const body = await response.json().catch(() => null) as { error?: string } | null; throw new Error(checkoutError(body?.error, response.status)); } const { checkoutId } = await response.json(); submitted.current = true; try { localStorage.removeItem(storageKey); } catch { /* Navigation remains available when storage is blocked. */ } router.push(`/commission/pay/${checkoutId}`); } catch (cause) { const message = cause instanceof Error ? cause.message : "สร้างคำขอไม่สำเร็จ"; setError(message); } finally { submitting.current = false; setBusy(false); } } return
คำขอคำนวณทีม อาวุธ และกลุ่มดาว

{draftNotice}

ระยะเวลาจัดทำ 1–7 วันตามขนาดคำขอ งานเล็กอาจเสร็จภายในประมาณ 4 ชั่วโมงหลังยืนยันการชำระเงิน

{attempted && issues.some((issue) => !issue.path) && เพิ่มทีมหรือรายการเปรียบเทียบอย่างน้อยหนึ่งรายการ} ทีม - ฿100 ต่อทีมเลือก 4 ตัวละคร พร้อมอาวุธและอาร์ติแฟกต์ของแต่ละตัว {request.teams.map((team, teamIndex) => ทีม {teamIndex + 1}
{team.members.map((member, memberIndex) => { const character = catalog.characters.find((item) => item.key === member.characterKey); return
#{memberIndex + 1}
{ const teams = structuredClone(request.teams); teams[teamIndex].members[memberIndex].constellation = constellation as typeof member.constellation; patch({ teams }); }} triggerProps={{ size: "sm", className: "h-6 min-w-0 flex-1 justify-center px-1 text-xs [&>svg]:hidden" }} /> { const teams = structuredClone(request.teams); teams[teamIndex].members[memberIndex].refinement = refinement as typeof member.refinement; patch({ teams }); }} triggerProps={{ size: "sm", className: "h-6 min-w-0 flex-1 justify-center px-1 text-xs [&>svg]:hidden" }} />

{character?.name ?? "เลือกตัวละคร"}

{errorsAt(`teams.${teamIndex}.members.${memberIndex}`).length > 0 && {errorsAt(`teams.${teamIndex}.members.${memberIndex}`).join(" - ")}}
; })}
{issues.some((issue) => issue.path === `teams.${teamIndex}`) && {issues.filter((issue) => issue.path === `teams.${teamIndex}`).map((issue) => issue.message).join(" - ")}}
)}
เปรียบเทียบอาวุธ - ฿20 ต่ออาวุธเลือกตัวละครและอาวุธอย่างน้อย 2 ชิ้น หากไม่มีทีมจะมีค่าบริการพื้นฐาน ฿100 {request.weapons.map((set, index) =>
{set.weaponKeys.map((weaponKey, weaponIndex) => { return
!set.weaponKeys.includes(item.key) || item.key === weaponKey)} value={weaponKey} invalid={invalid(`weapons.${index}.weaponKeys.${weaponIndex}`)} label={`เลือกอาวุธเปรียบเทียบชิ้นที่ ${weaponIndex + 1}`} placeholder={`อาวุธชิ้นที่ ${weaponIndex + 1}`} emptyLabel={set.characterKey ? "ไม่พบอาวุธ" : "เลือกตัวละครก่อน"} onValueChange={(next) => { const weapons = structuredClone(request.weapons); delete weapons[index].refinements?.[weaponKey]; weapons[index].weaponKeys[weaponIndex] = next; if (next) weapons[index].refinements = { ...weapons[index].refinements, [next]: "1" }; patch({ weapons }); }} />
{ const weapons = structuredClone(request.weapons); weapons[index].refinements = { ...weapons[index].refinements, [weaponKey]: refinement as "1" | "2" | "3" | "4" | "5" }; patch({ weapons }); }} triggerProps={{ size: "sm", className: "w-16 shrink-0 px-1 text-xs" }} />
; })}
{errorsAt(`weapons.${index}`).length > 0 && {errorsAt(`weapons.${index}`).join(" - ")}}
)}
เปรียบเทียบกลุ่มดาว - ฿20 ต่อระดับเลือกตัวละครและกลุ่มดาวอย่างน้อย 1 ระดับ หากไม่มีทีมจะมีค่าบริการพื้นฐาน ฿100 {request.constellations.map((set, index) =>
{Array.from({ length: 7 }, (_, level) => )}
{errorsAt(`constellations.${index}`).length > 0 && {errorsAt(`constellations.${index}`).join(" - ")}}
)}
สรุปคำขอ {request.teams.length} ทีม - {request.weapons.reduce((sum, set) => sum + set.weaponKeys.filter(Boolean).length, 0)} อาวุธ - {request.constellations.reduce((sum, set) => sum + set.levels.length, 0)} ระดับกลุ่มดาว {error && {error}}
รวม ฿{commissionPrice(request)}

{request.teams.length} ทีม - {request.weapons.reduce((sum, set) => sum + set.weaponKeys.filter(Boolean).length, 0)} อาวุธ - {request.constellations.reduce((sum, set) => sum + set.levels.length, 0)} ระดับกลุ่มดาว

{request.teams.length === 0 && request.weapons.length + request.constellations.length > 0 &&

รวมค่าบริการพื้นฐาน ฿100

}
; }