fix(guide) : link catalog items correctly
This commit is contained in:
@@ -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"),
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user