feat : buzz feedback
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
TimerIcon,
|
||||
Trash2Icon,
|
||||
UserRoundIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import {
|
||||
@@ -86,6 +87,7 @@ import {
|
||||
import {
|
||||
normalizeTargetStat,
|
||||
targetStatDefaults,
|
||||
targetStatIconName,
|
||||
targetStatOptions,
|
||||
} from "@/lib/guides/stats";
|
||||
import { supportsWeaponType } from "@/lib/catalog/lunaris";
|
||||
@@ -122,24 +124,29 @@ export interface GuideSectionEditorProps {
|
||||
artifactProfile: ArtifactProfile;
|
||||
constellations: ConstellationValue[];
|
||||
teams: TeamValue[];
|
||||
extras: Array<{ title: string; rows: Array<{ mediaId: string | null; heading: string; body: string; percent: string | null; imageUrl: string | null }> }>;
|
||||
extras: Array<{ title: string; rows: Array<{ mediaId: string | null; ratio: "1:1" | "16:9"; heading: string; body: string; percent: string | null; imageUrl: string | null }> }>;
|
||||
};
|
||||
}
|
||||
|
||||
const SELECTED_TOGGLE_CLASS = "aria-pressed:border-primary aria-pressed:ring-2 aria-pressed:ring-primary/30";
|
||||
|
||||
function CatalogPicker({ items, value, onChange, placeholder, rarityBackground = false }: { items: CatalogItem[]; value: string; onChange: (value: string) => void; placeholder: string; rarityBackground?: boolean }) {
|
||||
function CatalogPicker({ items, value, onChange, placeholder, rarityBackground = false, showSelectedImage = false }: { items: CatalogItem[]; value: string; onChange: (value: string) => void; placeholder: string; rarityBackground?: boolean; showSelectedImage?: boolean }) {
|
||||
const selected = items.find((item) => item.key === value) ?? null;
|
||||
return (
|
||||
<Combobox items={items} value={selected} onValueChange={(next) => onChange(next?.key ?? "")} itemToStringLabel={(item) => item.name} itemToStringValue={(item) => item.key} autoHighlight>
|
||||
<ComboboxInput className="w-full" placeholder={placeholder} showClear />
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>ไม่พบรายการ</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => <ComboboxItem key={item.key} value={item}><div className={cn("relative size-8 shrink-0 overflow-hidden rounded-md", rarityBackground && rarityGradientClass(item.rarity))}><Image src={item.imageUrl} alt="" fill className="object-contain" /></div>{item.name}</ComboboxItem>}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{showSelectedImage && selected ? <div className={cn("relative size-10 shrink-0 overflow-hidden rounded-md", rarityBackground && rarityGradientClass(selected.rarity))}><Image src={selected.imageUrl} alt="" fill className="object-contain" /></div> : null}
|
||||
<div className="min-w-0 flex-1">
|
||||
<Combobox items={items} value={selected} onValueChange={(next) => onChange(next?.key ?? "")} itemToStringLabel={(item) => item.name} itemToStringValue={(item) => item.key} autoHighlight>
|
||||
<ComboboxInput className="w-full" placeholder={placeholder} showClear />
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>ไม่พบรายการ</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => <ComboboxItem key={item.key} value={item}><div className={cn("relative size-8 shrink-0 overflow-hidden rounded-md", rarityBackground && rarityGradientClass(item.rarity))}><Image src={item.imageUrl} alt="" fill className="object-contain" /></div>{item.name}</ComboboxItem>}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,14 +155,26 @@ function StatSelect({ label, options, value, max, onChange }: { label: string; o
|
||||
<Field>
|
||||
<FieldLabel>{label}</FieldLabel>
|
||||
<Select multiple value={value} onValueChange={(next) => { if (next.length <= max) onChange(next); }}>
|
||||
<SelectTrigger className="w-full"><SelectValue placeholder="Select recommended stats…" /></SelectTrigger>
|
||||
<SelectContent><SelectGroup>{options.map((option) => <SelectItem key={option} value={option} disabled={value.length >= max && !value.includes(option)}>{option}</SelectItem>)}</SelectGroup></SelectContent>
|
||||
<SelectTrigger className="w-full"><StatIcons values={value} /><SelectValue placeholder="Select recommended stats…" /></SelectTrigger>
|
||||
<SelectContent><SelectGroup>{options.map((option) => <SelectItem key={option} value={option} disabled={value.length >= max && !value.includes(option)}><StatIcons values={[option]} />{option}</SelectItem>)}</SelectGroup></SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>เลือกได้สูงสุด {max} ค่า</FieldDescription>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
function StatIcons({ values }: { values: readonly string[] }) {
|
||||
const iconNames = values.flatMap((value) => {
|
||||
const targetIcon = targetStatIconName(value);
|
||||
if (targetIcon) return [targetIcon];
|
||||
if (value === "CRIT Rate%/CRIT DMG%") return ["critrate", "critdmg"];
|
||||
const base = value.replace(/%$/, "").toLowerCase();
|
||||
return ["hp", "atk", "def", "em", "er"].includes(base) ? [base] : [];
|
||||
});
|
||||
if (!iconNames.length) return null;
|
||||
return <span className="flex shrink-0 items-center gap-0.5" aria-hidden="true">{iconNames.map((iconName) => <Image key={iconName} src={`/icon/stats/${iconName}.webp`} alt="" width={20} height={20} className="size-5 object-contain brightness-0 invert" />)}</span>;
|
||||
}
|
||||
|
||||
function TalentLevelPicker({ value, onChange }: { value?: string; onChange: (value?: string) => void }) {
|
||||
const selected = talentLevelOptions.find((level) => level === value) ?? null;
|
||||
return (
|
||||
@@ -348,7 +367,7 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Artifact tier list</CardTitle><CardDescription>จัดชุด Artifact ในกลุ่ม Late Game และ Early Game ระบบจะบันทึกอัตโนมัติ</CardDescription></CardHeader>
|
||||
<CardContent className="grid gap-5 md:grid-cols-2">
|
||||
{groups.map((group, groupIndex) => <Card key={group.name} size="sm"><CardHeader><CardTitle>{group.name}</CardTitle></CardHeader><CardContent className="flex flex-col gap-3">{group.rows.map((row, rowIndex) => <div key={rowIndex} className="flex flex-col gap-3 rounded-lg border p-3"><div className="grid gap-3 lg:grid-cols-[1fr_1fr_8rem_auto]"><CatalogPicker items={artifacts} value={row.firstArtifactKey} placeholder="ค้นหาชุด Artifact แรก…" rarityBackground 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 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 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></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>)}<Button variant="outline" onClick={() => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: [...item.rows, { firstArtifactKey: "", secondArtifactKey: null, percent: "100", note: "" }] } : item))}><PlusIcon data-icon="inline-start" />เพิ่มชุด Artifact</Button></CardContent></Card>)}
|
||||
{groups.map((group, groupIndex) => <Card key={group.name} size="sm"><CardHeader><CardTitle>{group.name}</CardTitle></CardHeader><CardContent className="flex flex-col gap-3">{group.rows.map((row, rowIndex) => <div key={rowIndex} className={cn("flex flex-col gap-3 rounded-lg border p-3", rowIndex % 2 === 0 ? "bg-muted/60" : "bg-muted/20")}><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 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></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>)}<Button variant="outline" onClick={() => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: [...item.rows, { firstArtifactKey: "", secondArtifactKey: null, percent: "100", note: "" }] } : item))}><PlusIcon data-icon="inline-start" />เพิ่มชุด Artifact</Button></CardContent></Card>)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -365,8 +384,8 @@ function ArtifactEditor({ guide, artifacts, initial, catalogTalents }: EditorBas
|
||||
{targets.map((target, index) => (
|
||||
<div key={`${target.stat}-${index}`} className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<Select value={target.stat} onValueChange={(stat) => { if (stat) setTargets(targets.map((item, itemIndex) => itemIndex === index ? { ...item, stat } : item)); }}>
|
||||
<SelectTrigger className="w-full" aria-label={`Target stat ${index + 1}`}><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectGroup>{targetStatOptions.map((option) => <SelectItem key={option} value={option} disabled={targets.some((item, itemIndex) => itemIndex !== index && item.stat === option)}>{option}</SelectItem>)}</SelectGroup></SelectContent>
|
||||
<SelectTrigger className="w-full" aria-label={`Target stat ${index + 1}`}><StatIcons values={[target.stat]} /><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectGroup>{targetStatOptions.map((option) => <SelectItem key={option} value={option} disabled={targets.some((item, itemIndex) => itemIndex !== index && item.stat === option)}><StatIcons values={[option]} />{option}</SelectItem>)}</SelectGroup></SelectContent>
|
||||
</Select>
|
||||
<Input value={target.value} placeholder="Value or text" aria-label={`${target.stat} value`} onChange={(event) => setTargets(targets.map((item, itemIndex) => itemIndex === index ? { ...item, value: event.target.value } : item))} />
|
||||
</div>
|
||||
@@ -475,28 +494,30 @@ function TeamEditor({ guide, characters, weapons, artifacts, initial }: EditorBa
|
||||
return (
|
||||
<SortableTeamItem key={team.clientId} id={team.clientId}>{(dragHandle) => <>
|
||||
<div className="flex items-center gap-2">
|
||||
<AccordionTrigger className="min-w-0 py-4 hover:no-underline">
|
||||
<AccordionTrigger className="min-w-0 flex-1 py-4 hover:no-underline">
|
||||
<span className="flex min-w-0 flex-col gap-1">
|
||||
<span className="font-semibold">{team.title || `Team ${teamIndex + 1}`}</span>
|
||||
<span className="text-xs font-normal text-muted-foreground">Damage {total.toLocaleString()} • DPS {dps.toFixed(0)}</span>
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
{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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<AccordionContent className="pb-4">
|
||||
<FieldGroup>
|
||||
@@ -544,13 +565,13 @@ function TeamEditor({ guide, characters, weapons, artifacts, initial }: EditorBa
|
||||
|
||||
function ExtraEditor({ guide, initial }: EditorBase) {
|
||||
const [sections, setSections] = useState(initial.extras);
|
||||
const value = { sections: sections.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ mediaId: row.mediaId, heading: row.heading, body: row.body, percent: row.percent === null ? null : Number(row.percent) })) })) };
|
||||
const value = { sections: sections.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ mediaId: row.mediaId, ratio: row.ratio, heading: row.heading, body: row.body, percent: row.percent === null ? null : Number(row.percent) })) })) };
|
||||
const autosave = useAutosave({ value, valid: extraSectionSchema.safeParse({ ...value, expectedVersion: guide.version }).success, initialVersion: guide.version, persist: (next, expectedVersion) => saveExtraSectionList(guide.id, { ...next, expectedVersion }) });
|
||||
return (
|
||||
<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="relative aspect-video overflow-hidden rounded-lg border"><Image src={row.imageUrl} alt={row.heading} fill className="object-cover" /></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><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, 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><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>)}
|
||||
</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} /></CardFooter>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user