fix(extra) : show text colors and catalog items
This commit is contained in:
@@ -22,5 +22,5 @@ export default async function StructuredEditorPage({ params }: { params: Promise
|
|||||||
const [catalog, content] = await Promise.all([getActiveCatalog(), getAdminGuideContent(guide.slug)]);
|
const [catalog, content] = await Promise.all([getActiveCatalog(), getAdminGuideContent(guide.slug)]);
|
||||||
if (!catalog || !content) notFound();
|
if (!catalog || !content) notFound();
|
||||||
const guideCharacter = catalog.characters.find((item) => item.key === guide.characterKey);
|
const guideCharacter = catalog.characters.find((item) => item.key === guide.characterKey);
|
||||||
return <div className="min-h-svh"><AdminHeader /><main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href={`/admin/${character}`} />} nativeButton={false} aria-label="กลับหน้า Guide"><ArrowLeftIcon /></Button><div className="min-w-0"><p className="text-sm text-muted-foreground">{guide.name}</p><h1 className="whitespace-nowrap font-heading text-3xl font-semibold">{guide.sections.find((section) => section.slug === page)?.title ?? page}</h1></div></header><GuideSectionNav characterKey={guide.characterKey} current={page as (typeof sections)[number]} /><GuideSectionEditor section={page as (typeof sections)[number]} guide={guide} weapons={catalog.weapons} artifacts={catalog.artifacts} characters={catalog.characters} catalogConstellations={guideCharacter?.constellations ?? []} catalogTalents={guideCharacter?.talents ?? []} initial={{ sectionNote: guide.sections.find((section) => 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 })) })) }} /></main></div>;
|
return <div className="min-h-svh"><AdminHeader /><main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href={`/admin/${character}`} />} nativeButton={false} aria-label="กลับหน้า Guide"><ArrowLeftIcon /></Button><div className="min-w-0"><p className="text-sm text-muted-foreground">{guide.name}</p><h1 className="whitespace-nowrap font-heading text-3xl font-semibold">{guide.sections.find((section) => section.slug === page)?.title ?? page}</h1></div></header><GuideSectionNav characterKey={guide.characterKey} current={page as (typeof sections)[number]} /><GuideSectionEditor section={page as (typeof sections)[number]} guide={guide} weapons={catalog.weapons} artifacts={catalog.artifacts} characters={catalog.characters} catalogConstellations={guideCharacter?.constellations ?? []} catalogTalents={guideCharacter?.talents ?? []} initial={{ sectionNote: guide.sections.find((section) => 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 })) })) }} /></main></div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { updateTag } from "next/cache";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import type { AutosaveResponse } from "@/lib/editor/autosave";
|
import type { AutosaveResponse } from "@/lib/editor/autosave";
|
||||||
|
import { pageCacheTag } from "@/lib/cache/tags";
|
||||||
import {
|
import {
|
||||||
createNamedRevision,
|
createNamedRevision,
|
||||||
saveDataSourceVersion,
|
saveDataSourceVersion,
|
||||||
@@ -263,7 +265,9 @@ export async function saveTeamSection(guideId: string, input: unknown) {
|
|||||||
|
|
||||||
export async function saveExtraSectionList(guideId: string, input: unknown) {
|
export async function saveExtraSectionList(guideId: string, input: unknown) {
|
||||||
await requireAdmin();
|
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) {
|
export async function moveGuideToTrash(guideId: string) {
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ type TeamValue = {
|
|||||||
|
|
||||||
let clientIdCounter = 0;
|
let clientIdCounter = 0;
|
||||||
const DEFAULT_EXTRA_BACKGROUND_COLOR = "#24232C";
|
const DEFAULT_EXTRA_BACKGROUND_COLOR = "#24232C";
|
||||||
|
const DEFAULT_EXTRA_TEXT_COLOR = "#FAFAFA";
|
||||||
const RichTextEditor = dynamic(
|
const RichTextEditor = dynamic(
|
||||||
() => import("@/components/rich-text-editor").then((module) => module.RichTextEditor),
|
() => import("@/components/rich-text-editor").then((module) => module.RichTextEditor),
|
||||||
{
|
{
|
||||||
@@ -329,6 +330,7 @@ export interface GuideSectionEditorProps {
|
|||||||
body: string;
|
body: string;
|
||||||
percent: string | null;
|
percent: string | null;
|
||||||
backgroundColor: string | null;
|
backgroundColor: string | null;
|
||||||
|
textColor: string | null;
|
||||||
catalogItems: ExtraCatalogItem[];
|
catalogItems: ExtraCatalogItem[];
|
||||||
highlights: Array<{ text: string; color: string }>;
|
highlights: Array<{ text: string; color: string }>;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
@@ -3423,6 +3425,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB
|
|||||||
? null
|
? null
|
||||||
: Number(row.percent),
|
: Number(row.percent),
|
||||||
backgroundColor: row.backgroundColor,
|
backgroundColor: row.backgroundColor,
|
||||||
|
textColor: row.textColor,
|
||||||
catalogItems: row.catalogItems,
|
catalogItems: row.catalogItems,
|
||||||
highlights: [],
|
highlights: [],
|
||||||
})),
|
})),
|
||||||
@@ -3455,6 +3458,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB
|
|||||||
id: row.id || createClientId("extra-row-record"),
|
id: row.id || createClientId("extra-row-record"),
|
||||||
body: normalizeExtraRichText(row.body, row.highlights ?? []),
|
body: normalizeExtraRichText(row.body, row.highlights ?? []),
|
||||||
backgroundColor: row.backgroundColor ?? null,
|
backgroundColor: row.backgroundColor ?? null,
|
||||||
|
textColor: row.textColor ?? null,
|
||||||
catalogItems: row.catalogItems ?? [],
|
catalogItems: row.catalogItems ?? [],
|
||||||
highlights: [],
|
highlights: [],
|
||||||
clientId: row.clientId || createClientId("extra-row"),
|
clientId: row.clientId || createClientId("extra-row"),
|
||||||
@@ -3850,7 +3854,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<FieldSet>
|
<FieldSet>
|
||||||
<FieldLegend>สีพื้นหลังการ์ด (optional)</FieldLegend>
|
<FieldLegend>สีการ์ด (optional)</FieldLegend>
|
||||||
<FieldDescription>
|
<FieldDescription>
|
||||||
สีจะใช้เฉพาะการ์ดนี้บนหน้า Guide
|
สีจะใช้เฉพาะการ์ดนี้บนหน้า Guide
|
||||||
</FieldDescription>
|
</FieldDescription>
|
||||||
@@ -3917,6 +3921,69 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB
|
|||||||
<RefreshCcwIcon data-icon="inline-start" />
|
<RefreshCcwIcon data-icon="inline-start" />
|
||||||
ใช้สีตามธีม
|
ใช้สีตามธีม
|
||||||
</Button>
|
</Button>
|
||||||
|
<Field
|
||||||
|
data-invalid={extraIssue(
|
||||||
|
"sections",
|
||||||
|
sectionIndex,
|
||||||
|
"rows",
|
||||||
|
rowIndex,
|
||||||
|
"textColor",
|
||||||
|
) || undefined}
|
||||||
|
>
|
||||||
|
<FieldLabel>สีข้อความ</FieldLabel>
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
value={row.textColor ?? DEFAULT_EXTRA_TEXT_COLOR}
|
||||||
|
aria-invalid={extraIssue(
|
||||||
|
"sections",
|
||||||
|
sectionIndex,
|
||||||
|
"rows",
|
||||||
|
rowIndex,
|
||||||
|
"textColor",
|
||||||
|
)}
|
||||||
|
onChange={(event) =>
|
||||||
|
setSections(
|
||||||
|
sections.map((item, index) =>
|
||||||
|
index === sectionIndex
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
rows: item.rows.map((candidate, inner) =>
|
||||||
|
inner === rowIndex
|
||||||
|
? { ...candidate, textColor: event.target.value }
|
||||||
|
: candidate,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={row.textColor === null}
|
||||||
|
onClick={() =>
|
||||||
|
setSections(
|
||||||
|
sections.map((item, index) =>
|
||||||
|
index === sectionIndex
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
rows: item.rows.map((candidate, inner) =>
|
||||||
|
inner === rowIndex
|
||||||
|
? { ...candidate, textColor: null }
|
||||||
|
: candidate,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RefreshCcwIcon data-icon="inline-start" />
|
||||||
|
ใช้สีข้อความตามธีม
|
||||||
|
</Button>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -3947,6 +4014,7 @@ function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorB
|
|||||||
body: "",
|
body: "",
|
||||||
percent: null,
|
percent: null,
|
||||||
backgroundColor: null,
|
backgroundColor: null,
|
||||||
|
textColor: null,
|
||||||
catalogItems: [],
|
catalogItems: [],
|
||||||
highlights: [],
|
highlights: [],
|
||||||
clientId: createClientId("extra-row"),
|
clientId: createClientId("extra-row"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowUpRightIcon, ChevronLeftIcon } from "lucide-react";
|
import { ArrowUpRightIcon, ChevronLeftIcon } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -1180,7 +1180,16 @@ function ExtraSectionContent({
|
|||||||
<Card
|
<Card
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className="overflow-hidden py-0"
|
className="overflow-hidden py-0"
|
||||||
style={row.backgroundColor ? { backgroundColor: row.backgroundColor } : undefined}
|
style={
|
||||||
|
{
|
||||||
|
backgroundColor: row.backgroundColor ?? undefined,
|
||||||
|
color: row.textColor ?? undefined,
|
||||||
|
"--tw-prose-body": row.textColor ?? undefined,
|
||||||
|
"--tw-prose-bold": row.textColor ?? undefined,
|
||||||
|
"--tw-prose-headings": row.textColor ?? undefined,
|
||||||
|
"--tw-prose-links": row.textColor ?? undefined,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{row.imageUrl ? (
|
{row.imageUrl ? (
|
||||||
<div
|
<div
|
||||||
@@ -1219,7 +1228,7 @@ function ExtraSectionContent({
|
|||||||
) : null}
|
) : null}
|
||||||
<CardHeader className="pt-5">
|
<CardHeader className="pt-5">
|
||||||
<CardTitle>{row.heading}</CardTitle>
|
<CardTitle>{row.heading}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription style={{ color: row.textColor ?? undefined }}>
|
||||||
<RichTextContent html={normalizeExtraRichText(row.body, row.highlights)} />
|
<RichTextContent html={normalizeExtraRichText(row.body, row.highlights)} />
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
{row.percent ? <Badge>{metric(row.percent)}</Badge> : null}
|
{row.percent ? <Badge>{metric(row.percent)}</Badge> : null}
|
||||||
|
|||||||
@@ -146,15 +146,17 @@ describe("structured guide validation", () => {
|
|||||||
body: "First line\nSecond line",
|
body: "First line\nSecond line",
|
||||||
percent: null,
|
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,
|
expectedVersion: 1,
|
||||||
sections: [{ title: "Extra", rows: [{ ...row, ...style }] }],
|
sections: [{ title: "Extra", rows: [{ ...row, ...style }] }],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(extraSectionSchema.safeParse(input({ backgroundColor: null, highlights: [] })).success).toBe(true);
|
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(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({ 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: "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: "red" }] })).success).toBe(false);
|
||||||
expect(extraSectionSchema.safeParse(input({ backgroundColor: null, highlights: [{ text: "MASSIVE", color: "#FF0000" }, { text: "MASSIVE", color: "#00FF00" }] })).success).toBe(false);
|
expect(extraSectionSchema.safeParse(input({ backgroundColor: null, highlights: [{ text: "MASSIVE", color: "#FF0000" }, { text: "MASSIVE", color: "#00FF00" }] })).success).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ const extraRowSchema = z.strictObject({
|
|||||||
body: z.string().trim().max(10_000),
|
body: z.string().trim().max(10_000),
|
||||||
percent: percent.nullable(),
|
percent: percent.nullable(),
|
||||||
backgroundColor: hexColor.nullable().default(null),
|
backgroundColor: hexColor.nullable().default(null),
|
||||||
|
textColor: hexColor.nullable().default(null),
|
||||||
catalogItems: z.array(z.strictObject({
|
catalogItems: z.array(z.strictObject({
|
||||||
kind: z.enum(["character", "weapon", "artifact"]),
|
kind: z.enum(["character", "weapon", "artifact"]),
|
||||||
key: z.string().trim().min(1).max(64),
|
key: z.string().trim().min(1).max(64),
|
||||||
|
|||||||
Reference in New Issue
Block a user