From 86882dc338607ba2d45ccdb1d2d95e84ca84da3a Mon Sep 17 00:00:00 2001 From: gunshiz Date: Sat, 5 Sep 2026 13:09:55 +0700 Subject: [PATCH] fix(extra) : show text colors and catalog items --- app/admin/[character]/[page]/page.tsx | 2 +- app/admin/actions.ts | 6 +- components/admin/guide-section-editor.tsx | 70 ++++++++++++++++++++++- components/public/structured-guide.tsx | 15 ++++- lib/guides/schemas.test.ts | 4 +- lib/guides/schemas.ts | 1 + 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/app/admin/[character]/[page]/page.tsx b/app/admin/[character]/[page]/page.tsx index e0939c8..22dfffd 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) => ({ 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, catalogItems: row.catalogItems.map(({ kind, key }) => ({ kind, key })), highlights: row.highlights, 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, catalogItems: row.catalogItems.map(({ kind, key }) => ({ kind, key })), highlights: row.highlights, imageUrl: row.imageUrl, imageWidth: row.imageWidth, imageHeight: row.imageHeight })) })) }} />
; } diff --git a/app/admin/actions.ts b/app/admin/actions.ts index d107aa1..2dc82d4 100644 --- a/app/admin/actions.ts +++ b/app/admin/actions.ts @@ -1,8 +1,10 @@ "use server"; +import { updateTag } from "next/cache"; import * as z from "zod"; import type { AutosaveResponse } from "@/lib/editor/autosave"; +import { pageCacheTag } from "@/lib/cache/tags"; import { createNamedRevision, saveDataSourceVersion, @@ -263,7 +265,9 @@ export async function saveTeamSection(guideId: string, input: unknown) { export async function saveExtraSectionList(guideId: string, input: unknown) { await requireAdmin(); - return runGuideAction(() => saveExtraSections(guideId, input)); + const result = await runGuideAction(() => saveExtraSections(guideId, input)); + if (result.status === "saved") updateTag(pageCacheTag(guideId)); + return result; } export async function moveGuideToTrash(guideId: string) { diff --git a/components/admin/guide-section-editor.tsx b/components/admin/guide-section-editor.tsx index f083212..ac77733 100644 --- a/components/admin/guide-section-editor.tsx +++ b/components/admin/guide-section-editor.tsx @@ -269,6 +269,7 @@ type TeamValue = { let clientIdCounter = 0; const DEFAULT_EXTRA_BACKGROUND_COLOR = "#24232C"; +const DEFAULT_EXTRA_TEXT_COLOR = "#FAFAFA"; const RichTextEditor = dynamic( () => import("@/components/rich-text-editor").then((module) => module.RichTextEditor), { @@ -329,6 +330,7 @@ export interface GuideSectionEditorProps { body: string; percent: string | null; backgroundColor: string | null; + textColor: string | null; catalogItems: ExtraCatalogItem[]; highlights: Array<{ text: string; color: string }>; imageUrl: string | null; @@ -3423,6 +3425,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB ? null : Number(row.percent), backgroundColor: row.backgroundColor, + textColor: row.textColor, catalogItems: row.catalogItems, highlights: [], })), @@ -3455,6 +3458,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB id: row.id || createClientId("extra-row-record"), body: normalizeExtraRichText(row.body, row.highlights ?? []), backgroundColor: row.backgroundColor ?? null, + textColor: row.textColor ?? null, catalogItems: row.catalogItems ?? [], highlights: [], clientId: row.clientId || createClientId("extra-row"), @@ -3850,7 +3854,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB />
- สีพื้นหลังการ์ด (optional) + สีการ์ด (optional) สีจะใช้เฉพาะการ์ดนี้บนหน้า Guide @@ -3917,6 +3921,69 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB ใช้สีตามธีม + + สีข้อความ + + setSections( + sections.map((item, index) => + index === sectionIndex + ? { + ...item, + rows: item.rows.map((candidate, inner) => + inner === rowIndex + ? { ...candidate, textColor: event.target.value } + : candidate, + ), + } + : item, + ), + ) + } + /> + +
@@ -3947,6 +4014,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB body: "", percent: null, backgroundColor: null, + textColor: null, catalogItems: [], highlights: [], clientId: createClientId("extra-row"), diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index dbea238..bfb7af3 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -1,7 +1,7 @@ import Image from "next/image"; import Link from "next/link"; import { ArrowUpRightIcon, ChevronLeftIcon } from "lucide-react"; -import type { ReactNode } from "react"; +import type { CSSProperties, ReactNode } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -1180,7 +1180,16 @@ function ExtraSectionContent({ {row.imageUrl ? (
{row.heading} - + {row.percent ? {metric(row.percent)} : null} diff --git a/lib/guides/schemas.test.ts b/lib/guides/schemas.test.ts index 71bee25..f014727 100644 --- a/lib/guides/schemas.test.ts +++ b/lib/guides/schemas.test.ts @@ -146,15 +146,17 @@ describe("structured guide validation", () => { body: "First line\nSecond line", percent: null, }; - const input = (style: { backgroundColor: string | null; highlights: Array<{ text: string; color: string }> }) => ({ + const input = (style: { backgroundColor: string | null; textColor?: string | null; highlights: Array<{ text: string; color: string }> }) => ({ expectedVersion: 1, sections: [{ title: "Extra", rows: [{ ...row, ...style }] }], }); expect(extraSectionSchema.safeParse(input({ backgroundColor: null, highlights: [] })).success).toBe(true); + expect(extraSectionSchema.safeParse(input({ backgroundColor: "#24232C", textColor: "#FAFAFA", highlights: [] })).success).toBe(true); expect(extraSectionSchema.safeParse(input({ backgroundColor: "#24232C", highlights: [{ text: "MASSIVE", color: "#FF0000" }] })).success).toBe(true); expect(extraSectionSchema.safeParse({ expectedVersion: 1, sections: [{ title: "Extra", rows: [row] }] }).success).toBe(true); expect(extraSectionSchema.safeParse(input({ backgroundColor: "red", highlights: [] })).success).toBe(false); + expect(extraSectionSchema.safeParse(input({ backgroundColor: null, textColor: "white", highlights: [] })).success).toBe(false); expect(extraSectionSchema.safeParse(input({ backgroundColor: null, highlights: [{ text: "MASSIVE", color: "red" }] })).success).toBe(false); expect(extraSectionSchema.safeParse(input({ backgroundColor: null, highlights: [{ text: "MASSIVE", color: "#FF0000" }, { text: "MASSIVE", color: "#00FF00" }] })).success).toBe(false); }); diff --git a/lib/guides/schemas.ts b/lib/guides/schemas.ts index 2006e02..70a8acb 100644 --- a/lib/guides/schemas.ts +++ b/lib/guides/schemas.ts @@ -140,6 +140,7 @@ const extraRowSchema = z.strictObject({ body: z.string().trim().max(10_000), percent: percent.nullable(), backgroundColor: hexColor.nullable().default(null), + textColor: hexColor.nullable().default(null), catalogItems: z.array(z.strictObject({ kind: z.enum(["character", "weapon", "artifact"]), key: z.string().trim().min(1).max(64),