diff --git a/app/admin/[character]/[page]/page.tsx b/app/admin/[character]/[page]/page.tsx index eb4b015..68ee29b 100644 --- a/app/admin/[character]/[page]/page.tsx +++ b/app/admin/[character]/[page]/page.tsx @@ -22,5 +22,5 @@ export default async function StructuredEditorPage({ params }: { params: Promise const [catalog, content] = await Promise.all([getActiveCatalog(), getAdminGuideContent(guide.slug)]); if (!catalog || !content) notFound(); const guideCharacter = catalog.characters.find((item) => item.key === guide.characterKey); - return

{guide.name}

{guide.sections.find((section) => section.slug === page)?.title ?? page}

section.slug === page)?.note ?? null, weaponGroups: content.weaponGroups, artifactGroups: content.artifactGroups.map((group) => ({ id: group.id, name: group.name, rows: group.rows.map((row) => ({ firstArtifactKey: row.firstArtifactKey, secondArtifactKey: row.secondArtifactKey, percent: row.percent, note: row.note })) })), artifactProfile: content.artifactProfile, constellationGroups: content.constellationGroups.map((group) => ({ id: group.id, note: group.note, rows: group.rows })), teams: content.teams.map((team) => ({ id: team.id, title: team.title, rotationSeconds: team.rotationSeconds, combo: team.combo, members: team.members.map((member) => ({ characterKey: member.characterKey, weaponKey: member.weaponKey, artifactKey: member.artifactKey, secondArtifactKey: member.secondArtifactKey, constellation: member.constellation, refinement: member.refinement, damage: member.damage })) })), extras: content.extras.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ mediaId: row.mediaId, heading: row.heading, body: row.body, percent: row.percent, imageUrl: row.imageUrl, imageWidth: row.imageWidth, imageHeight: row.imageHeight })) })) }} />
; + return

{guide.name}

{guide.sections.find((section) => section.slug === page)?.title ?? page}

section.slug === page)?.note ?? null, weaponGroups: content.weaponGroups, artifactGroups: content.artifactGroups.map((group) => ({ id: group.id, name: group.name, rows: group.rows.map((row) => ({ firstArtifactKey: row.firstArtifactKey, secondArtifactKey: row.secondArtifactKey, percent: row.percent, note: row.note })) })), artifactProfile: content.artifactProfile, constellationGroups: content.constellationGroups.map((group) => ({ id: group.id, note: group.note, rows: group.rows })), teams: content.teams.map((team) => ({ id: team.id, title: team.title, rotationSeconds: team.rotationSeconds, combo: team.combo, members: team.members.map((member) => ({ characterKey: member.characterKey, weaponKey: member.weaponKey, artifactKey: member.artifactKey, secondArtifactKey: member.secondArtifactKey, constellation: member.constellation, refinement: member.refinement, damage: member.damage })) })), extras: content.extras.map((section) => ({ id: section.id, title: section.title, rows: section.rows.map((row) => ({ id: row.id, mediaId: row.mediaId, heading: row.heading, body: row.body, percent: row.percent, backgroundColor: row.backgroundColor, textColor: row.textColor, imageUrl: row.imageUrl, imageWidth: row.imageWidth, imageHeight: row.imageHeight })) })) }} />
; } diff --git a/app/globals.css b/app/globals.css index 976f399..997fade 100644 --- a/app/globals.css +++ b/app/globals.css @@ -130,6 +130,11 @@ } } +::view-transition-group(.guide-nav-pill) { + animation-duration: 220ms; + animation-timing-function: ease-in-out; +} + @media (prefers-reduced-motion: reduce) { *, *::before, @@ -139,4 +144,11 @@ animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } + + ::view-transition-old(*), + ::view-transition-new(*), + ::view-transition-group(*) { + animation-delay: 0s !important; + animation-duration: 0.01ms !important; + } } diff --git a/components/admin/guide-section-editor.tsx b/components/admin/guide-section-editor.tsx index 1dcee61..dc039ab 100644 --- a/components/admin/guide-section-editor.tsx +++ b/components/admin/guide-section-editor.tsx @@ -259,6 +259,8 @@ type TeamValue = { }; let clientIdCounter = 0; +const DEFAULT_EXTRA_BACKGROUND_COLOR = "#24232C"; +const DEFAULT_EXTRA_TEXT_COLOR = "#F5F2EA"; function createClientId(prefix: string): string { const browserCrypto = globalThis.crypto; @@ -303,12 +305,16 @@ export interface GuideSectionEditorProps { constellationGroups: ConstellationGroupValue[]; teams: TeamValue[]; extras: Array<{ + id: string; title: string; rows: Array<{ + id: string; mediaId: string | null; heading: string; body: string; percent: string | null; + backgroundColor: string | null; + textColor: string | null; imageUrl: string | null; imageWidth: number | null; imageHeight: number | null; @@ -769,6 +775,46 @@ function SortableTeamMember({ ); } +function SortableExtraItem({ + id, + label, + children, +}: { + id: string; + label: string; + children: (dragHandle: ReactNode) => ReactNode; +}) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + return ( +
+ {children( + , + )} +
+ ); +} + export function GuideSectionEditor(props: GuideSectionEditorProps) { return (
@@ -3250,7 +3296,22 @@ function TeamEditor({ } function ExtraEditor({ guide, initial }: EditorBase) { - const [sections, setSections] = useState(initial.extras); + const extraSensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + const [sections, setSections] = useState(() => + initial.extras.map((section) => ({ + ...section, + clientId: section.id, + rows: section.rows.map((row) => ({ + ...row, + clientId: row.id, + })), + })), + ); const value = { sections: sections.map((section) => ({ title: section.title, @@ -3262,6 +3323,8 @@ function ExtraEditor({ guide, initial }: EditorBase) { row.percent === null || row.percent === "" ? null : Number(row.percent), + backgroundColor: row.backgroundColor, + textColor: row.textColor, })), })), }; @@ -3282,8 +3345,40 @@ function ExtraEditor({ guide, initial }: EditorBase) { localDraft, parseLocalDraft: (candidate) => parseEditorDraft(candidate, ["sections"]), - restoreLocalDraft: (draft) => setSections(draft.sections), + restoreLocalDraft: (draft) => setSections( + draft.sections.map((section) => ({ + ...section, + id: section.id || createClientId("extra-section-record"), + clientId: section.clientId || createClientId("extra-section"), + rows: section.rows.map((row) => ({ + ...row, + id: row.id || createClientId("extra-row-record"), + backgroundColor: row.backgroundColor ?? null, + textColor: row.textColor ?? null, + clientId: row.clientId || createClientId("extra-row"), + })), + })), + ), }); + const handleSectionDragEnd = ({ active, over }: DragEndEvent) => { + if (!over || active.id === over.id) return; + setSections((current) => { + const from = current.findIndex((section) => section.clientId === active.id); + const to = current.findIndex((section) => section.clientId === over.id); + return from === -1 || to === -1 ? current : arrayMove(current, from, to); + }); + }; + const handleRowDragEnd = (sectionId: string, { active, over }: DragEndEvent) => { + if (!over || active.id === over.id) return; + setSections((current) => current.map((section) => { + if (section.clientId !== sectionId) return section; + const from = section.rows.findIndex((row) => row.clientId === active.id); + const to = section.rows.findIndex((row) => row.clientId === over.id); + return from === -1 || to === -1 + ? section + : { ...section, rows: arrayMove(section.rows, from, to) }; + })); + }; return ( @@ -3294,9 +3389,23 @@ function ExtraEditor({ guide, initial }: EditorBase) { - {sections.map((section, sectionIndex) => ( + + section.clientId)} + strategy={verticalListSortingStrategy} + > + {sections.map((section, sectionIndex) => ( + + {(sectionDragHandle) => ( - + + {sectionDragHandle} @@ -3338,9 +3448,23 @@ function ExtraEditor({ guide, initial }: EditorBase) { - {section.rows.map((row, rowIndex) => ( + handleRowDragEnd(section.clientId, event)} + > + row.clientId)} + strategy={verticalListSortingStrategy} + > + {section.rows.map((row, rowIndex) => ( + + {(rowDragHandle) => ( {row.heading || `แถว ${rowIndex + 1}`} - + + {rowDragHandle} +
+ สีการ์ด (optional) + + สีจะใช้เฉพาะการ์ดนี้บนหน้า Guide + + + + สีพื้นหลัง + + setSections( + sections.map((item, index) => + index === sectionIndex + ? { + ...item, + rows: item.rows.map((candidate, inner) => + inner === rowIndex + ? { + ...candidate, + backgroundColor: event.target.value, + textColor: candidate.textColor ?? DEFAULT_EXTRA_TEXT_COLOR, + } + : candidate, + ), + } + : item, + ), + ) + } + /> + + + สีข้อความ + + setSections( + sections.map((item, index) => + index === sectionIndex + ? { + ...item, + rows: item.rows.map((candidate, inner) => + inner === rowIndex + ? { + ...candidate, + backgroundColor: candidate.backgroundColor ?? DEFAULT_EXTRA_BACKGROUND_COLOR, + textColor: event.target.value, + } + : candidate, + ), + } + : item, + ), + ) + } + /> + + + +
- ))} + )} +
+ ))} +
+