"use client"; import Image from "next/image"; import { ArrowDownIcon, ArrowUpIcon, PlusIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; import { saveArtifactSection, saveConstellationSection, saveExtraSectionList, saveTeamSection, saveWeaponSection, } from "@/app/admin/actions"; import { AutosaveStatus } from "@/components/admin/autosave-status"; import { MediaUploader } from "@/components/admin/media-uploader"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@/components/ui/combobox"; import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useAutosave } from "@/hooks/use-autosave"; import { artifactSectionSchema, constellationSectionSchema, extraSectionSchema, mainStatOptions, substatOptions, teamSectionSchema, weaponSectionSchema, } from "@/lib/guides/schemas"; import { supportsWeaponType } from "@/lib/catalog/lunaris"; import { rarityGradientClass } from "@/lib/catalog/rarity"; import { cn } from "@/lib/utils"; type CatalogItem = { key: string; name: string; imageUrl: string; rarity?: number | null; weaponType?: string | null }; type Guide = { id: string; characterKey: string; version: number; name: string }; type WeaponValue = { weaponKey: string; refinement: number; overallPercent: string | null; personalPercent: string | null; weapon: CatalogItem }; type ArtifactValue = { id: string; name: string; rows: Array<{ firstArtifactKey: string; secondArtifactKey: string | null; percent: string }> }; type ArtifactProfile = { sands: string[]; goblet: string[]; circlet: string[]; substats: string[]; targets: Array<{ stat: string; value: string }>; talents: Array<{ type: "NA" | "E" | "Q"; priority: number; level?: string }>; note: string | null } | null; type ConstellationValue = { level: number; detail: string; cumulativePercent: string }; type CatalogConstellation = { level: number; name: string; description: string; iconUrl: string | null }; type TeamValue = { title: string; rotationSeconds: string; combo: string; members: Array<{ characterKey: string; weaponKey: string | null; artifactKey: string | null; constellation: number; refinement: number; damage: string }> }; export interface GuideSectionEditorProps { section: "weapon" | "artifact" | "constellations" | "team" | "extra"; guide: Guide; weapons: CatalogItem[]; artifacts: CatalogItem[]; characters: CatalogItem[]; catalogConstellations: CatalogConstellation[]; initial: { weapons: WeaponValue[]; artifactGroups: ArtifactValue[]; 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 }> }>; }; } const DEFAULT_TARGET_STATS = ["ATK", "EM", "ER", "CRIT Rate", "CRIT DMG"] as const; function CatalogPicker({ items, value, onChange, placeholder, rarityBackground = false }: { items: CatalogItem[]; value: string; onChange: (value: string) => void; placeholder: string; rarityBackground?: boolean }) { const selected = items.find((item) => item.key === value) ?? null; return ( onChange(next?.key ?? "")} itemToStringLabel={(item) => item.name} itemToStringValue={(item) => item.key}> ไม่พบรายการ {items.map((item) =>
{item.name}
)}
); } function StatSelect({ label, options, value, max, onChange }: { label: string; options: readonly string[]; value: string[]; max: number; onChange: (value: string[]) => void }) { return ( {label} เลือกได้สูงสุด {max} ค่า ); } function RowActions({ index, length, move, remove }: { index: number; length: number; move: (from: number, to: number) => void; remove?: () => void }) { return
{remove ? : null}
; } function moveItem(items: T[], from: number, to: number): T[] { const next = [...items]; const [item] = next.splice(from, 1); next.splice(to, 0, item); return next; } export function GuideSectionEditor(props: GuideSectionEditorProps) { return (
{props.section === "weapon" ? : null} {props.section === "artifact" ? : null} {props.section === "constellations" ? : null} {props.section === "team" ? : null} {props.section === "extra" ? : null}
); } type EditorBase = GuideSectionEditorProps; function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) { const characterWeaponType = characters.find((character) => character.key === guide.characterKey)?.weaponType; const compatibleWeapons = weapons.filter((weapon) => supportsWeaponType(characterWeaponType, weapon.weaponType)); const [rows, setRows] = useState(initial.weapons.map((row) => ({ weaponKey: row.weaponKey, refinement: row.refinement, overallPercent: row.overallPercent ?? "", personalPercent: row.personalPercent ?? "" }))); const value = { rows: rows.map((row) => ({ ...row, overallPercent: row.overallPercent === "" ? null : Number(row.overallPercent), personalPercent: row.personalPercent === "" ? null : Number(row.personalPercent) })) }; const autosave = useAutosave({ value, valid: weaponSectionSchema.safeParse({ ...value, expectedVersion: guide.version }).success, initialVersion: guide.version, persist: (next, expectedVersion) => saveWeaponSection(guide.id, { ...next, expectedVersion }) }); return ( Weapon tier listเรียงอาวุธจากดีที่สุดลงมา และกรอกเปอร์เซ็นต์อย่างน้อยหนึ่งช่อง ระบบจะบันทึกอัตโนมัติ {rows.map((row, rowIndex) => { const selectedWeapon = compatibleWeapons.find((weapon) => weapon.key === row.weaponKey); return {selectedWeapon ?
: null}อันดับ {rowIndex + 1}
setRows(moveItem(rows, from, to))} remove={() => setRows(rows.filter((_, index) => index !== rowIndex))} />
อาวุธ setRows(rows.map((item, index) => index === rowIndex ? { ...item, weaponKey } : item))} />Refinement { const next = selected[0]; if (next) setRows(rows.map((item, index) => index === rowIndex ? { ...item, refinement: Number(next.slice(1)) } : item)); }}>{[1, 2, 3, 4, 5].map((refinement) => R{refinement})}
Overall % setRows(rows.map((item, index) => index === rowIndex ? { ...item, overallPercent: event.target.value } : item))} />Personal % setRows(rows.map((item, index) => index === rowIndex ? { ...item, personalPercent: event.target.value } : item))} />
; })}
); } function ArtifactEditor({ guide, artifacts, initial }: EditorBase) { const profile = initial.artifactProfile; const [groups, setGroups] = useState(initial.artifactGroups.map((group) => ({ name: group.name, rows: group.rows.map((row) => ({ ...row })) }))); const [sands, setSands] = useState(profile?.sands ?? []); const [goblet, setGoblet] = useState(profile?.goblet ?? []); const [circlet, setCirclet] = useState(profile?.circlet ?? []); const [substats, setSubstats] = useState(profile?.substats ?? []); const [targets, setTargets] = useState(() => { const existing = profile?.targets ?? []; const defaults = DEFAULT_TARGET_STATS.map((stat) => existing.find((target) => target.stat === stat) ?? { stat, value: "" }); return [...defaults, ...existing.filter((target) => !DEFAULT_TARGET_STATS.includes(target.stat as (typeof DEFAULT_TARGET_STATS)[number]))]; }); const [talents, setTalents] = useState(profile?.talents ?? (["NA", "E", "Q"] as const).map((type, priority) => ({ type, priority: priority + 1, level: "" }))); const [note, setNote] = useState(profile?.note ?? ""); const value = { groups: groups.map((group) => ({ ...group, rows: group.rows.map((row) => ({ ...row, percent: Number(row.percent) })) })), sands, goblet, circlet, substats, targets: targets.filter((target) => target.stat.trim() && target.value.trim()), talents, note: note || null }; const autosave = useAutosave({ value, valid: artifactSectionSchema.safeParse({ ...value, expectedVersion: guide.version }).success, initialVersion: guide.version, persist: (next, expectedVersion) => saveArtifactSection(guide.id, { ...next, expectedVersion }) }); const orderedTalents = [...talents].sort((a, b) => a.priority - b.priority); return (
Artifact tier listสร้างกลุ่มและค้นหาชุด Artifact ด้วยชื่อ ระบบจะบันทึกอัตโนมัติ {groups.map((group, groupIndex) => setGroups(groups.map((item, index) => index === groupIndex ? { ...item, name: event.target.value } : item))} />{group.rows.map((row, rowIndex) =>
setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, firstArtifactKey } : candidate) } : item))} /> setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, secondArtifactKey: secondArtifactKey || null } : candidate) } : item))} /> setGroups(groups.map((item, index) => index === groupIndex ? { ...item, rows: item.rows.map((candidate, inner) => inner === rowIndex ? { ...candidate, percent: event.target.value } : candidate) } : item))} />
)}
)}
Recommended Statsเลือกค่าจากรายการแทนปุ่ม และเลือก Main Stat ได้สูงสุด 2 ค่าต่อชิ้น
Target Stats{targets.map((target, index) =>
setTargets(targets.map((item, itemIndex) => itemIndex === index ? { ...item, stat: event.target.value } : item))} /> setTargets(targets.map((item, itemIndex) => itemIndex === index ? { ...item, value: event.target.value } : item))} />
)}
Talent Priority{orderedTalents.map((talent, index) =>
{index + 1}. {talent.type} setTalents(talents.map((item) => item.type === talent.type ? { ...item, level: event.target.value } : item))} /> setTalents(moveItem(orderedTalents, from, to).map((item, priority) => ({ ...item, priority: priority + 1 })))} />
)}
Note (optional)