|
|
|
@@ -3,6 +3,7 @@
|
|
|
|
|
import Image from "next/image";
|
|
|
|
|
import {
|
|
|
|
|
ChartNoAxesCombinedIcon,
|
|
|
|
|
CopyIcon,
|
|
|
|
|
GemIcon,
|
|
|
|
|
GripVerticalIcon,
|
|
|
|
|
HeartPulseIcon,
|
|
|
|
@@ -44,19 +45,9 @@ import {
|
|
|
|
|
saveWeaponSection,
|
|
|
|
|
} from "@/app/admin/actions";
|
|
|
|
|
import { AutosaveStatus } from "@/components/admin/autosave-status";
|
|
|
|
|
import { DeleteConfirmationDialog } from "@/components/admin/delete-confirmation-dialog";
|
|
|
|
|
import { MediaUploader } from "@/components/admin/media-uploader";
|
|
|
|
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
|
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
AlertDialogTrigger,
|
|
|
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
@@ -366,7 +357,7 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) {
|
|
|
|
|
<DndContext sensors={weaponSensors} collisionDetection={closestCenter} onDragEnd={handleWeaponDragEnd}>
|
|
|
|
|
<SortableContext items={rows.map((row) => row.clientId)} strategy={verticalListSortingStrategy}>
|
|
|
|
|
<div className="flex flex-col gap-4">
|
|
|
|
|
{rows.map((row, rowIndex) => { const selectedWeapon = compatibleWeapons.find((weapon) => weapon.key === row.weaponKey); return <SortableWeaponCard key={row.clientId} id={row.clientId}>{(dragHandle) => <Card size="sm"><CardHeader><CardTitle className="flex items-center gap-3">{selectedWeapon ? <div className={cn("relative size-10 shrink-0 overflow-hidden rounded-lg", rarityGradientClass(selectedWeapon.rarity))}><Image src={selectedWeapon.imageUrl} alt="" fill className="object-contain" /></div> : null}<span>อันดับ {rowIndex + 1}</span></CardTitle><CardAction><div className="flex gap-1">{dragHandle}<Button type="button" variant="ghost" size="icon-sm" onClick={() => setRows(rows.filter((item) => item.clientId !== row.clientId))}><Trash2Icon /><span className="sr-only">ลบอาวุธ</span></Button></div></CardAction></CardHeader><CardContent><FieldGroup><div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-4"><Field className="min-w-0"><FieldLabel>อาวุธ</FieldLabel><CatalogPicker items={compatibleWeapons} value={row.weaponKey} placeholder="ค้นหาชื่ออาวุธ…" rarityBackground onChange={(weaponKey) => setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, weaponKey } : item))} /></Field><Field className="shrink-0"><FieldLabel>Refinement</FieldLabel><ToggleGroup variant="outline" value={[`R${row.refinement}`]} onValueChange={(selected) => { const next = selected[0]; if (next) setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, refinement: Number(next.slice(1)) } : item)); }}>{[1, 2, 3, 4, 5].map((refinement) => <ToggleGroupItem className={SELECTED_TOGGLE_CLASS} key={refinement} value={`R${refinement}`}>R{refinement}</ToggleGroupItem>)}</ToggleGroup></Field></div><div className="grid gap-4 sm:grid-cols-2"><Field><FieldLabel>Overall %</FieldLabel><Input type="number" min="0" step="0.01" value={row.overallPercent} onChange={(event) => setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, overallPercent: event.target.value } : item))} /></Field><Field><FieldLabel>Personal %</FieldLabel><Input type="number" min="0" step="0.01" value={row.personalPercent} onChange={(event) => setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, personalPercent: event.target.value } : item))} /></Field></div><Field><FieldLabel htmlFor={`weapon-note-${row.clientId}`}>Note (optional)</FieldLabel><Textarea id={`weapon-note-${row.clientId}`} value={row.note} onChange={(event) => setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, note: event.target.value } : item))} /></Field></FieldGroup></CardContent></Card>}</SortableWeaponCard>; })}
|
|
|
|
|
{rows.map((row, rowIndex) => { const selectedWeapon = compatibleWeapons.find((weapon) => weapon.key === row.weaponKey); return <SortableWeaponCard key={row.clientId} id={row.clientId}>{(dragHandle) => <Card size="sm"><CardHeader><CardTitle className="flex items-center gap-3">{selectedWeapon ? <div className={cn("relative size-10 shrink-0 overflow-hidden rounded-lg", rarityGradientClass(selectedWeapon.rarity))}><Image src={selectedWeapon.imageUrl} alt="" fill className="object-contain" /></div> : null}<span>อันดับ {rowIndex + 1}</span></CardTitle><CardAction><div className="flex gap-1">{dragHandle}<DeleteConfirmationDialog trigger={<Button type="button" variant="ghost" size="icon-sm" />} title={`ลบ ${selectedWeapon?.name ?? `อาวุธอันดับ ${rowIndex + 1}`}?`} description="รายการอาวุธและเปอร์เซ็นต์เปรียบเทียบจะถูกลบออกจาก Guide" onConfirm={() => setRows(rows.filter((item) => item.clientId !== row.clientId))}><Trash2Icon /><span className="sr-only">ลบอาวุธ</span></DeleteConfirmationDialog></div></CardAction></CardHeader><CardContent><FieldGroup><div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-4"><Field className="min-w-0"><FieldLabel>อาวุธ</FieldLabel><CatalogPicker items={compatibleWeapons} value={row.weaponKey} placeholder="ค้นหาชื่ออาวุธ…" rarityBackground onChange={(weaponKey) => setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, weaponKey } : item))} /></Field><Field className="shrink-0"><FieldLabel>Refinement</FieldLabel><ToggleGroup variant="outline" value={[`R${row.refinement}`]} onValueChange={(selected) => { const next = selected[0]; if (next) setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, refinement: Number(next.slice(1)) } : item)); }}>{[1, 2, 3, 4, 5].map((refinement) => <ToggleGroupItem className={SELECTED_TOGGLE_CLASS} key={refinement} value={`R${refinement}`}>R{refinement}</ToggleGroupItem>)}</ToggleGroup></Field></div><div className="grid gap-4 sm:grid-cols-2"><Field><FieldLabel>Overall %</FieldLabel><Input type="number" step="0.01" value={row.overallPercent} onChange={(event) => setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, overallPercent: event.target.value } : item))} /></Field><Field><FieldLabel>Personal %</FieldLabel><Input type="number" step="0.01" value={row.personalPercent} onChange={(event) => setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, personalPercent: event.target.value } : item))} /></Field></div><Field><FieldLabel htmlFor={`weapon-note-${row.clientId}`}>Note (optional)</FieldLabel><Textarea id={`weapon-note-${row.clientId}`} value={row.note} onChange={(event) => setRows(rows.map((item) => item.clientId === row.clientId ? { ...item, note: event.target.value } : item))} /></Field></FieldGroup></CardContent></Card>}</SortableWeaponCard>; })}
|
|
|
|
|
</div>
|
|
|
|
|
</SortableContext>
|
|
|
|
|
</DndContext>
|
|
|
|
@@ -470,8 +461,8 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
|
|
|
|
|
<div className="grid gap-3 lg:grid-cols-[1fr_1fr_8rem_auto]">
|
|
|
|
|
<CatalogPicker items={artifacts} value={row.firstArtifactKey} placeholder="ค้นหาชุด Artifact แรก…" rarityBackground showSelectedImage onChange={(firstArtifactKey) => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, firstArtifactKey } : candidate) } : item))} />
|
|
|
|
|
<CatalogPicker items={artifacts} value={row.secondArtifactKey ?? ""} placeholder="ค้นหาชุดที่สอง (ถ้ามี)…" rarityBackground showSelectedImage onChange={(secondArtifactKey) => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, secondArtifactKey: secondArtifactKey || null } : candidate) } : item))} />
|
|
|
|
|
<Input type="number" min="0" step="0.01" aria-label="Artifact percent" value={row.percent} onChange={(event) => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, percent: event.target.value } : candidate) } : item))} />
|
|
|
|
|
<Button type="button" variant="ghost" size="icon" onClick={() => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.filter((_, inner) => inner !== rowIndex) } : item))}><Trash2Icon /><span className="sr-only">ลบ Artifact</span></Button>
|
|
|
|
|
<Input type="number" step="0.01" aria-label="Artifact percent" value={row.percent} onChange={(event) => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, percent: event.target.value } : candidate) } : item))} />
|
|
|
|
|
<DeleteConfirmationDialog trigger={<Button type="button" variant="ghost" size="icon" />} title={`ลบ Artifact อันดับ ${rowIndex + 1}?`} description={`รายการ Artifact ในกลุ่ม ${group.name} และเปอร์เซ็นต์เปรียบเทียบจะถูกลบออกจาก Guide`} onConfirm={() => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.filter((_, inner) => inner !== rowIndex) } : item))}><Trash2Icon /><span className="sr-only">ลบ Artifact</span></DeleteConfirmationDialog>
|
|
|
|
|
</div>
|
|
|
|
|
<Field><FieldLabel htmlFor={`artifact-note-${groupIndex}-${rowIndex}`}>Note (optional)</FieldLabel><Textarea id={`artifact-note-${groupIndex}-${rowIndex}`} value={row.note ?? ""} onChange={(event) => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, note: event.target.value } : candidate) } : item))} /></Field>
|
|
|
|
|
</div>
|
|
|
|
@@ -599,11 +590,11 @@ function ConstellationEditor({ guide, initial, catalogConstellations }: EditorBa
|
|
|
|
|
<CardContent className="flex flex-col gap-4">
|
|
|
|
|
<Field><FieldLabel htmlFor="constellation-section-note">Header note (optional)</FieldLabel><Textarea id="constellation-section-note" value={note} onChange={(event) => setNote(event.target.value)} /></Field>
|
|
|
|
|
{groups.map((group, groupIndex) => <Card key={group.clientId} size="sm">
|
|
|
|
|
<CardHeader><CardTitle>Condition {groupIndex + 1}</CardTitle><CardDescription>ใช้ note เพื่ออธิบายอาวุธ ทีม หรือเงื่อนไขที่ใช้เปรียบเทียบ</CardDescription>{groups.length > 1 ? <CardAction><AlertDialog><AlertDialogTrigger render={<Button type="button" variant="ghost" size="icon-sm" />}><Trash2Icon /><span className="sr-only">ลบ Condition {groupIndex + 1}</span></AlertDialogTrigger><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>ลบ Condition {groupIndex + 1}?</AlertDialogTitle><AlertDialogDescription>เปอร์เซ็นต์กลุ่มดาวทั้งหมดในเงื่อนไขนี้จะถูกลบออก</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel>ยกเลิก</AlertDialogCancel><AlertDialogAction variant="destructive" onClick={() => setGroups(groups.filter((item) => item.clientId !== group.clientId))}>ยืนยันลบ</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog></CardAction> : null}</CardHeader>
|
|
|
|
|
<CardHeader><CardTitle>Condition {groupIndex + 1}</CardTitle><CardDescription>ใช้ note เพื่ออธิบายอาวุธ ทีม หรือเงื่อนไขที่ใช้เปรียบเทียบ</CardDescription>{groups.length > 1 ? <CardAction><DeleteConfirmationDialog trigger={<Button type="button" variant="ghost" size="icon-sm" />} title={`ลบ Condition ${groupIndex + 1}?`} description="เปอร์เซ็นต์กลุ่มดาวทั้งหมดในเงื่อนไขนี้จะถูกลบออก" onConfirm={() => setGroups(groups.filter((item) => item.clientId !== group.clientId))}><Trash2Icon /><span className="sr-only">ลบ Condition {groupIndex + 1}</span></DeleteConfirmationDialog></CardAction> : null}</CardHeader>
|
|
|
|
|
<CardContent className="flex flex-col gap-4">
|
|
|
|
|
<Field><FieldLabel htmlFor={`constellation-condition-note-${group.clientId}`}>Condition note (optional)</FieldLabel><Textarea id={`constellation-condition-note-${group.clientId}`} value={group.note} onChange={(event) => setGroups(groups.map((item) => item.clientId === group.clientId ? { ...item, note: event.target.value } : item))} /></Field>
|
|
|
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
|
|
|
{group.rows.map((row, rowIndex) => <Card key={row.level} size="sm"><CardHeader>{row.iconUrl ? <Image src={row.iconUrl} alt="" width={48} height={48} className="rounded-lg bg-muted" /> : null}<CardTitle>C{row.level} · {row.name}</CardTitle><CardDescription>{row.description}</CardDescription></CardHeader><CardContent className="flex items-end gap-3"><Field className="min-w-0 flex-1"><FieldLabel>Cumulative %</FieldLabel><Input type="number" min="0" step="0.01" value={row.cumulativePercent} onChange={(event) => setGroups(groups.map((item) => item.clientId === group.clientId ? { ...item, rows: item.rows.map((candidate, index) => index === rowIndex ? { ...candidate, cumulativePercent: event.target.value } : candidate) } : item))} /></Field><Toggle variant="outline" pressed={row.isGreat} onPressedChange={(isGreat) => setGroups(groups.map((item) => item.clientId === group.clientId ? { ...item, rows: item.rows.map((candidate, index) => index === rowIndex ? { ...candidate, isGreat } : candidate) } : item))} aria-label={`Mark Condition ${groupIndex + 1} C${row.level} percentage as great`} className={cn(SELECTED_TOGGLE_CLASS, "data-[state=on]:text-warning")}><SparklesIcon data-icon="inline-start" />Great %</Toggle></CardContent></Card>)}
|
|
|
|
|
{group.rows.map((row, rowIndex) => <Card key={row.level} size="sm"><CardHeader>{row.iconUrl ? <Image src={row.iconUrl} alt="" width={48} height={48} className="rounded-lg bg-muted" /> : null}<CardTitle>C{row.level} · {row.name}</CardTitle><CardDescription>{row.description}</CardDescription></CardHeader><CardContent className="flex items-end gap-3"><Field className="min-w-0 flex-1"><FieldLabel>Cumulative %</FieldLabel><Input type="number" step="0.01" value={row.cumulativePercent} onChange={(event) => setGroups(groups.map((item) => item.clientId === group.clientId ? { ...item, rows: item.rows.map((candidate, index) => index === rowIndex ? { ...candidate, cumulativePercent: event.target.value } : candidate) } : item))} /></Field><Toggle variant="outline" pressed={row.isGreat} onPressedChange={(isGreat) => setGroups(groups.map((item) => item.clientId === group.clientId ? { ...item, rows: item.rows.map((candidate, index) => index === rowIndex ? { ...candidate, isGreat } : candidate) } : item))} aria-label={`Mark Condition ${groupIndex + 1} C${row.level} percentage as great`} className={cn(SELECTED_TOGGLE_CLASS, "data-[state=on]:text-warning")}><SparklesIcon data-icon="inline-start" />Great %</Toggle></CardContent></Card>)}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>)}
|
|
|
|
@@ -662,21 +653,38 @@ function TeamEditor({ guide, characters, weapons, artifacts, initial }: EditorBa
|
|
|
|
|
</AccordionTrigger>
|
|
|
|
|
<div className="ml-auto flex shrink-0 items-center gap-1">
|
|
|
|
|
{dragHandle}
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger render={<Button type="button" variant="ghost" size="icon-sm" />}>
|
|
|
|
|
<Trash2Icon /><span className="sr-only">ลบ Team</span>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>ลบ {team.title || `Team ${teamIndex + 1}`}?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>ข้อมูล Team และตัวละครทั้ง 4 คนจะถูกลบออกจาก Guide</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>ยกเลิก</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction variant="destructive" onClick={() => setTeams(teams.filter((item) => item.clientId !== team.clientId))}>ยืนยันลบ Team</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
disabled={teams.length >= 30}
|
|
|
|
|
onClick={() => setTeams((current) => {
|
|
|
|
|
const sourceIndex = current.findIndex((item) => item.clientId === team.clientId);
|
|
|
|
|
if (sourceIndex === -1 || current.length >= 30) return current;
|
|
|
|
|
const source = current[sourceIndex];
|
|
|
|
|
const title = source.title.trim()
|
|
|
|
|
? `${source.title.slice(0, 153)} (Copy)`
|
|
|
|
|
: `Team ${sourceIndex + 1} (Copy)`;
|
|
|
|
|
const copy = {
|
|
|
|
|
...source,
|
|
|
|
|
clientId: createClientId("team"),
|
|
|
|
|
title,
|
|
|
|
|
members: source.members.map((member) => ({ ...member })),
|
|
|
|
|
};
|
|
|
|
|
return current.toSpliced(sourceIndex + 1, 0, copy);
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<CopyIcon /><span className="sr-only">ทำสำเนา Team</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<DeleteConfirmationDialog
|
|
|
|
|
trigger={<Button type="button" variant="ghost" size="icon-sm" />}
|
|
|
|
|
title={`ลบ ${team.title || `Team ${teamIndex + 1}`}?`}
|
|
|
|
|
description="ข้อมูล Team และตัวละครทั้ง 4 คนจะถูกลบออกจาก Guide"
|
|
|
|
|
confirmLabel="ยืนยันลบ Team"
|
|
|
|
|
onConfirm={() => setTeams(teams.filter((item) => item.clientId !== team.clientId))}
|
|
|
|
|
>
|
|
|
|
|
<Trash2Icon /><span className="sr-only">ลบ Team</span>
|
|
|
|
|
</DeleteConfirmationDialog>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<AccordionContent className="pb-4">
|
|
|
|
@@ -744,7 +752,72 @@ function ExtraEditor({ guide, initial }: EditorBase) {
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader><CardTitle>Extra sections</CardTitle><CardDescription>เพิ่มส่วนเสริมแบบภาพ หัวข้อ ข้อความสั้น และเปอร์เซ็นต์ ระบบจะบันทึกอัตโนมัติ</CardDescription></CardHeader>
|
|
|
|
|
<CardContent className="flex flex-col gap-5">
|
|
|
|
|
{sections.map((section, sectionIndex) => <Card key={sectionIndex} size="sm"><CardHeader><CardTitle><Input value={section.title} aria-label="ชื่อ Extra section" onChange={(event) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, title: event.target.value } : item))} /></CardTitle><CardAction><Button variant="ghost" size="icon-sm" onClick={() => setSections(sections.filter((_, index) => index !== sectionIndex))}><Trash2Icon /><span className="sr-only">ลบส่วน</span></Button></CardAction></CardHeader><CardContent className="flex flex-col gap-4">{section.rows.map((row, rowIndex) => <Card key={rowIndex} size="sm"><CardHeader><CardTitle>{row.heading || `แถว ${rowIndex + 1}`}</CardTitle><CardAction><Button variant="ghost" size="icon-sm" onClick={() => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.filter((_, inner) => inner !== rowIndex) } : item))}><Trash2Icon /><span className="sr-only">ลบแถว</span></Button></CardAction></CardHeader><CardContent><FieldGroup>{row.imageUrl ? <div className={cn("relative overflow-hidden rounded-lg border", row.ratio === "1:1" ? "aspect-square" : "aspect-video")}><Image src={row.imageUrl} alt={row.heading} fill className="object-cover" /><Button type="button" variant="destructive" size="icon-sm" className="absolute top-2 right-2" onClick={() => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, mediaId: null, imageUrl: null } : candidate) } : item))} aria-label="ลบภาพ"><XIcon /></Button></div> : null}<MediaUploader onReady={(asset) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, mediaId: asset.id, imageUrl: asset.url } : candidate) } : item))} /><Field><FieldLabel>อัตราส่วนภาพ</FieldLabel><ToggleGroup variant="outline" value={[row.ratio]} onValueChange={(selected) => { const ratio = selected[0]; if (ratio === "1:1" || ratio === "16:9") setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, ratio } : candidate) } : item)); }} className="flex-wrap justify-start"><ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="1:1">1:1</ToggleGroupItem><ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="16:9">16:9</ToggleGroupItem></ToggleGroup></Field><Field><FieldLabel>หัวข้อ</FieldLabel><Input value={row.heading} onChange={(event) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, heading: event.target.value } : candidate) } : item))} /></Field><Field><FieldLabel>ข้อความ</FieldLabel><Textarea value={row.body} onChange={(event) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, body: event.target.value } : candidate) } : item))} /></Field><Field><FieldLabel>Percent (optional)</FieldLabel><Input type="number" min="0" step="0.01" value={row.percent ?? ""} onChange={(event) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, percent: event.target.value || null } : candidate) } : item))} /></Field></FieldGroup></CardContent></Card>)}<Button variant="outline" onClick={() => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: [...item.rows, { mediaId: null, imageUrl: null, ratio: "16:9", heading: "", body: "", percent: null }] } : item))}><PlusIcon data-icon="inline-start" />เพิ่มแถว</Button></CardContent></Card>)}
|
|
|
|
|
{sections.map((section, sectionIndex) => (
|
|
|
|
|
<Card key={sectionIndex} size="sm">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle><Input value={section.title} aria-label="ชื่อ Extra section" onChange={(event) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, title: event.target.value } : item))} /></CardTitle>
|
|
|
|
|
<CardAction>
|
|
|
|
|
<DeleteConfirmationDialog
|
|
|
|
|
trigger={<Button type="button" variant="ghost" size="icon-sm" />}
|
|
|
|
|
title={`ลบ ${section.title || `Extra section ${sectionIndex + 1}`}?`}
|
|
|
|
|
description="แถว ข้อความ รูปภาพ และเปอร์เซ็นต์ทั้งหมดในส่วนนี้จะถูกลบออกจาก Guide"
|
|
|
|
|
onConfirm={() => setSections(sections.filter((_, index) => index !== sectionIndex))}
|
|
|
|
|
>
|
|
|
|
|
<Trash2Icon /><span className="sr-only">ลบส่วน</span>
|
|
|
|
|
</DeleteConfirmationDialog>
|
|
|
|
|
</CardAction>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="flex flex-col gap-4">
|
|
|
|
|
{section.rows.map((row, rowIndex) => (
|
|
|
|
|
<Card key={rowIndex} size="sm">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>{row.heading || `แถว ${rowIndex + 1}`}</CardTitle>
|
|
|
|
|
<CardAction>
|
|
|
|
|
<DeleteConfirmationDialog
|
|
|
|
|
trigger={<Button type="button" variant="ghost" size="icon-sm" />}
|
|
|
|
|
title={`ลบ ${row.heading || `แถว ${rowIndex + 1}`}?`}
|
|
|
|
|
description="ข้อความ รูปภาพ และเปอร์เซ็นต์ในแถวนี้จะถูกลบออกจาก Guide"
|
|
|
|
|
onConfirm={() => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.filter((_, inner) => inner !== rowIndex) } : item))}
|
|
|
|
|
>
|
|
|
|
|
<Trash2Icon /><span className="sr-only">ลบแถว</span>
|
|
|
|
|
</DeleteConfirmationDialog>
|
|
|
|
|
</CardAction>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<FieldGroup>
|
|
|
|
|
{row.imageUrl ? (
|
|
|
|
|
<div className={cn("relative overflow-hidden rounded-lg border", row.ratio === "1:1" ? "aspect-square" : "aspect-video")}>
|
|
|
|
|
<Image src={row.imageUrl} alt={row.heading} fill className="object-cover" />
|
|
|
|
|
<DeleteConfirmationDialog
|
|
|
|
|
trigger={<Button type="button" variant="destructive" size="icon-sm" className="absolute top-2 right-2" aria-label="ลบภาพ" />}
|
|
|
|
|
title="ลบภาพนี้?"
|
|
|
|
|
description="ภาพจะถูกนำออกจากแถวนี้"
|
|
|
|
|
confirmLabel="ยืนยันลบภาพ"
|
|
|
|
|
onConfirm={() => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, mediaId: null, imageUrl: null } : candidate) } : item))}
|
|
|
|
|
>
|
|
|
|
|
<XIcon />
|
|
|
|
|
</DeleteConfirmationDialog>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
<MediaUploader onReady={(asset) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, mediaId: asset.id, imageUrl: asset.url } : candidate) } : item))} />
|
|
|
|
|
<Field>
|
|
|
|
|
<FieldLabel>อัตราส่วนภาพ</FieldLabel>
|
|
|
|
|
<ToggleGroup variant="outline" value={[row.ratio]} onValueChange={(selected) => { const ratio = selected[0]; if (ratio === "1:1" || ratio === "16:9") setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, ratio } : candidate) } : item)); }} className="flex-wrap justify-start">
|
|
|
|
|
<ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="1:1">1:1</ToggleGroupItem>
|
|
|
|
|
<ToggleGroupItem className={SELECTED_TOGGLE_CLASS} value="16:9">16:9</ToggleGroupItem>
|
|
|
|
|
</ToggleGroup>
|
|
|
|
|
</Field>
|
|
|
|
|
<Field><FieldLabel>หัวข้อ</FieldLabel><Input value={row.heading} onChange={(event) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, heading: event.target.value } : candidate) } : item))} /></Field>
|
|
|
|
|
<Field><FieldLabel>ข้อความ</FieldLabel><Textarea value={row.body} onChange={(event) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, body: event.target.value } : candidate) } : item))} /></Field>
|
|
|
|
|
<Field><FieldLabel>Percent (optional)</FieldLabel><Input type="number" step="0.01" value={row.percent ?? ""} onChange={(event) => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, percent: event.target.value || null } : candidate) } : item))} /></Field>
|
|
|
|
|
</FieldGroup>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
<Button type="button" variant="outline" onClick={() => setSections(sections.map((item, index) => index === sectionIndex ? { ...item, rows: [...item.rows, { mediaId: null, imageUrl: null, ratio: "16:9", heading: "", body: "", percent: null }] } : item))}><PlusIcon data-icon="inline-start" />เพิ่มแถว</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
<CardFooter className="flex-wrap justify-between gap-3"><Button variant="outline" onClick={() => setSections([...sections, { title: "Extra", rows: [] }])}><PlusIcon data-icon="inline-start" />เพิ่ม Extra section</Button><AutosaveStatus state={autosave.state} issues={validation.success ? [] : validation.error.issues.map((issue) => issue.path.length ? `${issue.path.join(".")}: ${issue.message}` : issue.message)} onSave={autosave.saveNow} /></CardFooter>
|
|
|
|
|
</Card>
|
|
|
|
|