"use client"; import Image from "@/components/ui/resilient-image"; import { GripVerticalIcon, PlusIcon, Trash2Icon } from "lucide-react"; import { useState, type ReactNode } from "react"; import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { saveWeaponSection } from "@/app/admin/actions"; import { SaveStatus } from "@/components/admin/save-status"; import { DeleteConfirmationDialog } from "@/components/admin/ui/delete-confirmation-dialog"; import { WeaponSelect } from "@/components/admin/ui/catalog-select"; import { Button } from "@/components/ui/button"; import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; import { ClearableInput as Input } from "@/components/admin/ui/clearable-input"; import { Textarea } from "@/components/ui/textarea"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useManualSave } from "@/hooks/use-manual-save"; import { adminDraftKey } from "@/lib/editor/local-draft"; import { weaponSectionSchema } from "@/lib/guides/schemas"; import { supportsWeaponType } from "@/lib/catalog/lunaris"; import { rarityGradientClass } from "@/lib/catalog/rarity"; import { finalizePercentInput, normalizePercentInput } from "@/lib/guides/percent-input"; import { formatGuideValidationIssues } from "@/lib/guides/validation-messages"; import { cn } from "@/lib/utils"; import { type EditorBase, hasIssuePrefix, parseEditorDraft, ConditionControls, reorderConditions, createClientId, ConditionRenameDialog, SELECTED_TOGGLE_CLASS } from "./shared"; export function SortableWeaponCard({ id, children, }: { id: string; children: (dragHandle: ReactNode) => ReactNode; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }); return (
{children( , )}
); } export 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 weaponSensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const [note, setNote] = useState(initial.sectionNote ?? ""); const [groups, setGroups] = useState(() => { const source = initial.weaponGroups.length ? initial.weaponGroups : [{ id: "weapon-condition-default", name: null, note: null, iconMediaId: null, iconUrl: null, rows: [] }]; return source.map((group) => ({ clientId: group.id, name: group.name ?? "", note: group.note ?? "", iconMediaId: group.iconMediaId ?? null, iconUrl: group.iconUrl ?? null, rows: group.rows.map((row) => ({ clientId: row.id, weaponKey: row.weaponKey, refinement: row.refinement, overallPercent: row.overallPercent ?? "", personalPercent: row.personalPercent ?? "", note: row.note ?? "", })), })); }); const [activeGroupIndex, setActiveGroupIndex] = useState(0); const [renamingWeaponGroupIndex, setRenamingWeaponGroupIndex] = useState(null); const rows = groups[activeGroupIndex]?.rows ?? []; const setRows = (update: typeof rows | ((current: typeof rows) => typeof rows)) => setGroups((current) => current.map((group, index) => index === activeGroupIndex ? { ...group, rows: typeof update === "function" ? update(group.rows) : update } : group)); const value = { note: note.trim() || null, groups: groups.map((group) => ({ name: group.name.trim() || null, note: group.note.trim() || null, iconMediaId: group.iconMediaId, rows: group.rows.map((row) => ({ weaponKey: row.weaponKey, refinement: row.refinement, overallPercent: row.overallPercent === "" ? null : Number(row.overallPercent), personalPercent: row.personalPercent === "" ? null : Number(row.personalPercent), note: row.note.trim() || null, })), })), }; const validation = weaponSectionSchema.safeParse({ ...value, expectedVersion: guide.version, }); const weaponIssue = (...path: (string | number)[]) => !validation.success && hasIssuePrefix(validation.error.issues, ...path); const localDraft = { note, groups, activeGroupIndex }; const manualSave = useManualSave({ guideId: guide.id, scope: "weapon", value, valid: validation.success, initialVersion: guide.version, persist: (next, expectedVersion, intent) => saveWeaponSection(guide.id, { ...next, expectedVersion }, intent), storageKey: adminDraftKey(guide.id, "weapon"), localDraft, parseLocalDraft: (candidate) => parseEditorDraft(candidate, ["groups"], ["note"]), restoreLocalDraft: (draft) => { setNote(draft.note); setGroups(draft.groups.map((group) => ({ ...group, name: group.name ?? "" }))); setActiveGroupIndex(draft.activeGroupIndex ?? 0); }, }); const handleWeaponDragEnd = ({ active, over }: DragEndEvent) => { if (!over || active.id === over.id) return; setRows((current) => { const from = current.findIndex((row) => row.clientId === active.id); const to = current.findIndex((row) => row.clientId === over.id); return from === -1 || to === -1 ? current : arrayMove(current, from, to); }); }; return ( Weapon tier list ลากเพื่อเรียงอาวุธจากดีที่สุดลงมา และกรอกเปอร์เซ็นต์อย่างน้อยหนึ่งช่อง กดปุ่มบันทึกเพื่อบันทึกการเปลี่ยนแปลง Header note (optional)