From 1b4581b94e807277272c6f3d61017bf58fa0160f Mon Sep 17 00:00:00 2001 From: gunshiz Date: Tue, 1 Sep 2026 13:52:01 +0700 Subject: [PATCH] feat: add guide section header notes --- app/admin/[character]/[page]/page.tsx | 2 +- .../guides/[guideId]/weapons/image/route.tsx | 4 +- components/admin/guide-section-editor.tsx | 15 +- components/public/structured-guide.tsx | 6 +- db/schema.ts | 1 + drizzle/0017_needy_hulk.sql | 1 + drizzle/meta/0017_snapshot.json | 3228 +++++++++++++++++ drizzle/meta/_journal.json | 7 + lib/guides/mutations.ts | 2 + lib/guides/queries.ts | 3 +- lib/guides/schemas.test.ts | 19 +- lib/guides/schemas.ts | 2 + 12 files changed, 3276 insertions(+), 14 deletions(-) create mode 100644 drizzle/0017_needy_hulk.sql create mode 100644 drizzle/meta/0017_snapshot.json diff --git a/app/admin/[character]/[page]/page.tsx b/app/admin/[character]/[page]/page.tsx index b4d7f66..674fbe8 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(), getPublicGuide(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}

({ 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, constellations: content.constellations, 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, 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, ratio: row.ratio === "1:1" ? "1:1" : "16:9", heading: row.heading, body: row.body, percent: row.percent, imageUrl: row.imageUrl })) })) }} />
; + return

{guide.name}

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

section.slug === page)?.note ?? null, weapons: content.weapons, 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, constellations: content.constellations, 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, 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, ratio: row.ratio === "1:1" ? "1:1" : "16:9", heading: row.heading, body: row.body, percent: row.percent, imageUrl: row.imageUrl })) })) }} />
; } diff --git a/app/api/guides/[guideId]/weapons/image/route.tsx b/app/api/guides/[guideId]/weapons/image/route.tsx index 5e3fe0e..cf4bc53 100644 --- a/app/api/guides/[guideId]/weapons/image/route.tsx +++ b/app/api/guides/[guideId]/weapons/image/route.tsx @@ -57,7 +57,8 @@ export async function GET( Promise.all(guide.weapons.map((row) => pngDataUrl(row.weapon.imageUrl))), ]); const weaponHeights = guide.weapons.map((row) => Math.max(126, 126 + (row.note ? Math.ceil(row.note.length / 52) * 22 : 0))); - const height = Math.max(360, 190 + weaponHeights.reduce((sum, rowHeight) => sum + rowHeight, 0) + Math.max(0, guide.weapons.length - 1) * 14); + const noteHeight = guide.note ? Math.max(24, Math.ceil(guide.note.length / 70) * 22) : 0; + const height = Math.max(360, 190 + noteHeight + weaponHeights.reduce((sum, rowHeight) => sum + rowHeight, 0) + Math.max(0, guide.weapons.length - 1) * 14); return new ImageResponse(
อาวุธที่แนะนำ
อาวุธ
+ {guide.note ?
{guide.note}
: null}
{guide.weapons.map((row, index) => (
({ clientId: row.id, weaponKey: row.weaponKey, refinement: row.refinement, overallPercent: row.overallPercent ?? "", personalPercent: row.personalPercent ?? "", note: row.note ?? "" }))); - const value = { rows: 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 value = { note: note.trim() || null, rows: 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 autosave = useAutosave({ value, valid: validation.success, initialVersion: guide.version, persist: (next, expectedVersion) => saveWeaponSection(guide.id, { ...next, expectedVersion }) }); const handleWeaponDragEnd = ({ active, over }: DragEndEvent) => { @@ -341,6 +343,7 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) { Weapon tier listลากเพื่อเรียงอาวุธจากดีที่สุดลงมา และกรอกเปอร์เซ็นต์อย่างน้อยหนึ่งช่อง ระบบจะบันทึกอัตโนมัติ + Header note (optional)