fix(guide) : link catalog items correctly
CI / Build immutable images and deploy (push) Has been cancelled
CI / Verify (push) Has been cancelled

This commit is contained in:
2026-09-05 13:45:57 +07:00 Unverified
parent 5314996ef3
commit 912c94c298
4 changed files with 55 additions and 113 deletions
+1 -89
View File
@@ -269,7 +269,6 @@ 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),
{ {
@@ -330,7 +329,6 @@ 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;
@@ -3438,7 +3436,6 @@ 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: [],
})), })),
@@ -3471,7 +3468,6 @@ 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"),
@@ -3867,7 +3863,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>
@@ -3954,89 +3950,6 @@ 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",
)}
onClick={() => {
if (row.textColor !== null) return;
setSections(
sections.map((item, index) =>
index === sectionIndex
? {
...item,
rows: item.rows.map((candidate, inner) =>
inner === rowIndex
? {
...candidate,
textColor: DEFAULT_EXTRA_TEXT_COLOR,
}
: candidate,
),
}
: item,
),
);
}}
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>
@@ -4067,7 +3980,6 @@ 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"),
+53 -20
View File
@@ -1,10 +1,11 @@
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 { CSSProperties, ReactNode } from "react"; import type { 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";
import { Kbd } from "@/components/ui/kbd";
import { import {
Card, Card,
CardAction, CardAction,
@@ -313,6 +314,13 @@ function metric(value: string | null) {
: `${Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 })}%`; : `${Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 })}%`;
} }
function lunarisItemUrl(
kind: "character" | "weapon" | "artifact",
key: string,
): string {
return `https://lunaris.moe/${kind}/${encodeURIComponent(key)}`;
}
function WeaponSection({ guide }: { guide: PublicGuide }) { function WeaponSection({ guide }: { guide: PublicGuide }) {
return <WeaponSectionContent guide={guide} showExportActions />; return <WeaponSectionContent guide={guide} showExportActions />;
} }
@@ -435,7 +443,12 @@ function WeaponSectionContent({
: "md:grid-cols-[minmax(0,3fr)_minmax(0,1fr)]", : "md:grid-cols-[minmax(0,3fr)_minmax(0,1fr)]",
)} )}
> >
<div <a
href={lunarisItemUrl("weapon", row.weapon.key)}
target="_blank"
rel="noreferrer"
tabIndex={exporting ? -1 : undefined}
aria-label={`เปิดข้อมูลอาวุธ ${row.weapon.name} บน Lunaris`}
className={cn( className={cn(
"flex min-h-28 min-w-0 items-center gap-4 rounded-br-[4rem] p-4 pr-10 text-white", "flex min-h-28 min-w-0 items-center gap-4 rounded-br-[4rem] p-4 pr-10 text-white",
useCompactExportGrid useCompactExportGrid
@@ -480,7 +493,7 @@ function WeaponSectionContent({
</p> </p>
) : null} ) : null}
</div> </div>
</div> </a>
<div <div
className={cn( className={cn(
"flex items-center justify-around bg-white py-5 text-black", "flex items-center justify-around bg-white py-5 text-black",
@@ -719,7 +732,12 @@ function ArtifactSectionContent({
)} )}
> >
<div className="flex -space-x-3"> <div className="flex -space-x-3">
<div <a
href={lunarisItemUrl("artifact", row.first.key)}
target="_blank"
rel="noreferrer"
tabIndex={exporting ? -1 : undefined}
aria-label={`เปิดข้อมูลอาร์ติแฟกต์ ${row.first.name} บน Lunaris`}
className={cn( className={cn(
"relative overflow-hidden rounded-lg border", "relative overflow-hidden rounded-lg border",
exporting ? "size-16" : "size-14 sm:size-16", exporting ? "size-16" : "size-14 sm:size-16",
@@ -734,9 +752,19 @@ function ArtifactSectionContent({
sizes="(min-width: 640px) 4rem, 3.5rem" sizes="(min-width: 640px) 4rem, 3.5rem"
className="object-contain" className="object-contain"
/> />
</div> {!exporting ? (
<Kbd className="absolute top-0 right-0 h-3 min-w-3 bg-transparent p-0 text-white">
<ArrowUpRightIcon aria-hidden="true" />
</Kbd>
) : null}
</a>
{row.second ? ( {row.second ? (
<div <a
href={lunarisItemUrl("artifact", row.second.key)}
target="_blank"
rel="noreferrer"
tabIndex={exporting ? -1 : undefined}
aria-label={`เปิดข้อมูลอาร์ติแฟกต์ ${row.second.name} บน Lunaris`}
className={cn( className={cn(
"relative overflow-hidden rounded-lg border", "relative overflow-hidden rounded-lg border",
exporting ? "size-16" : "size-14 sm:size-16", exporting ? "size-16" : "size-14 sm:size-16",
@@ -751,7 +779,12 @@ function ArtifactSectionContent({
sizes="(min-width: 640px) 4rem, 3.5rem" sizes="(min-width: 640px) 4rem, 3.5rem"
className="object-contain" className="object-contain"
/> />
</div> {!exporting ? (
<Kbd className="absolute top-0 right-0 h-3 min-w-3 bg-transparent p-0 text-white">
<ArrowUpRightIcon aria-hidden="true" />
</Kbd>
) : null}
</a>
) : null} ) : null}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -1180,16 +1213,7 @@ function ExtraSectionContent({
<Card <Card
key={row.id} key={row.id}
className="overflow-hidden py-0" className="overflow-hidden py-0"
style={ style={row.backgroundColor ? { backgroundColor: row.backgroundColor } : undefined}
{
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
@@ -1212,7 +1236,15 @@ function ExtraSectionContent({
{row.catalogItems.length ? ( {row.catalogItems.length ? (
<CardContent className="grid grid-cols-2 gap-2 pt-5"> <CardContent className="grid grid-cols-2 gap-2 pt-5">
{row.catalogItems.map((item) => ( {row.catalogItems.map((item) => (
<div key={`${item.kind}:${item.key}`} className="flex min-w-0 items-center gap-2 rounded-lg border bg-background/80 p-2"> <a
key={`${item.kind}:${item.key}`}
href={lunarisItemUrl(item.kind, item.key)}
target="_blank"
rel="noreferrer"
tabIndex={exporting ? -1 : undefined}
aria-label={`เปิดข้อมูล ${item.name} บน Lunaris`}
className="flex min-w-0 items-center gap-2 rounded-lg border bg-background/80 p-2 outline-none hover:bg-background focus-visible:ring-2 focus-visible:ring-ring"
>
{item.imageUrl ? ( {item.imageUrl ? (
<div className={cn("relative size-12 shrink-0 overflow-hidden rounded-md", rarityGradientClass(item.rarity))}> <div className={cn("relative size-12 shrink-0 overflow-hidden rounded-md", rarityGradientClass(item.rarity))}>
<Image src={item.imageUrl} alt="" fill className="object-contain" /> <Image src={item.imageUrl} alt="" fill className="object-contain" />
@@ -1222,13 +1254,14 @@ function ExtraSectionContent({
<p className="truncate text-sm font-medium">{item.name}</p> <p className="truncate text-sm font-medium">{item.name}</p>
{!item.available ? <Badge variant="secondary">ไม่พบรายการ</Badge> : null} {!item.available ? <Badge variant="secondary">ไม่พบรายการ</Badge> : null}
</div> </div>
</div> <ArrowUpRightIcon aria-hidden="true" className="ml-auto shrink-0" />
</a>
))} ))}
</CardContent> </CardContent>
) : null} ) : null}
<CardHeader className="pt-5"> <CardHeader className="pt-5">
<CardTitle>{row.heading}</CardTitle> <CardTitle>{row.heading}</CardTitle>
<CardDescription style={{ color: row.textColor ?? undefined }}> <CardDescription>
<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}
+1 -3
View File
@@ -146,17 +146,15 @@ describe("structured guide validation", () => {
body: "First line\nSecond line", body: "First line\nSecond line",
percent: null, percent: null,
}; };
const input = (style: { backgroundColor: string | null; textColor?: string | null; highlights: Array<{ text: string; color: string }> }) => ({ const input = (style: { backgroundColor: 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);
}); });
-1
View File
@@ -140,7 +140,6 @@ 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),