feat : overhual [no ci]
This commit is contained in:
@@ -30,7 +30,7 @@ export function AdminHeader() {
|
||||
className="mr-auto flex min-h-11 items-center gap-3 rounded-lg font-heading font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Image src="/icon.png" alt="" width={36} height={36} className="size-9 object-contain" />
|
||||
<span className="hidden sm:inline">{exportText("watermark")}</span>
|
||||
<span className="hidden sm:inline">Buzz Guide</span>
|
||||
</Link>
|
||||
<LanguageDropdown />
|
||||
<nav aria-label="Admin navigation" className="flex items-center gap-1">
|
||||
|
||||
@@ -121,15 +121,15 @@ function SortableBleedingLayer({ layer, onChange, onRemove }: { layer: BleedingL
|
||||
items={blendModeItems}
|
||||
value={selectedMode}
|
||||
onValueChange={(value) => value && onChange({ blendMode: value.value })}
|
||||
itemToStringLabel={(item) => t(item.label)}
|
||||
itemToStringLabel={(item) => item.label}
|
||||
itemToStringValue={(item) => item.value}
|
||||
filter={(item, query) => `${t(item.label)} ${item.label} ${item.value}`.toLowerCase().includes(query.trim().toLowerCase())}
|
||||
filter={(item, query) => `${item.label} ${item.value}`.toLowerCase().includes(query.trim().toLowerCase())}
|
||||
>
|
||||
<ComboboxInput className="w-full" placeholder={t("เลือก Blend mode…")} />
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>{t("ไม่พบ Blend mode")}</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => <ComboboxItem key={item.value} value={item}>{t(item.label)}</ComboboxItem>}
|
||||
{(item) => <ComboboxItem key={item.value} value={item}>{item.label}</ComboboxItem>}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
@@ -100,7 +100,6 @@ import {
|
||||
weaponTypeIconPath,
|
||||
type OverviewRole,
|
||||
} from "@/lib/overview-image";
|
||||
import { withoutNotesFileName } from "@/lib/guides/export-text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Character = {
|
||||
@@ -649,10 +648,10 @@ function SortableBlendLayerRow({
|
||||
onValueChange={(value) => {
|
||||
if (value) onUpdate({ mode: value.value });
|
||||
}}
|
||||
itemToStringLabel={(item) => t(item.label)}
|
||||
itemToStringLabel={(item) => item.label}
|
||||
itemToStringValue={(item) => item.value}
|
||||
filter={(item, query) =>
|
||||
`${t(item.label)} ${item.label} ${item.value}`
|
||||
`${item.label} ${item.value}`
|
||||
.toLocaleLowerCase("en")
|
||||
.includes(query.trim().toLocaleLowerCase("en"))
|
||||
}
|
||||
@@ -663,7 +662,7 @@ function SortableBlendLayerRow({
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item.value} value={item}>
|
||||
{t(item.label)}
|
||||
{item.label}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
@@ -1006,7 +1005,7 @@ export function OverviewImageGenerator({
|
||||
}
|
||||
}
|
||||
|
||||
async function download(includeNotes = true) {
|
||||
async function download() {
|
||||
const node = captureRef.current;
|
||||
if (
|
||||
!node ||
|
||||
@@ -1038,7 +1037,7 @@ export function OverviewImageGenerator({
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
const fileName = overviewFileName(character.key, displayRole);
|
||||
link.download = includeNotes ? fileName : withoutNotesFileName(fileName);
|
||||
link.download = fileName;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
@@ -1343,15 +1342,6 @@ export function OverviewImageGenerator({
|
||||
)}
|
||||
{exporting ? e("exporting") : e("downloadPng", { width: 3000, height: 3000 })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={!ready || exporting}
|
||||
onClick={() => void download(false)}
|
||||
>
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
{e("withoutNote")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { cloneElement, type ReactElement } from "react";
|
||||
import { Children, cloneElement, type ReactElement } from "react";
|
||||
import { useTranslation } from "./locale-provider";
|
||||
import { exportStatLabel, type ExportMessageKey } from "@/lib/guides/export-text";
|
||||
import type { MessageParams } from "@/lib/i18n/messages";
|
||||
@@ -28,7 +28,9 @@ export function Localized({ children, attributes }: {
|
||||
const props = Object.fromEntries(Object.entries(attributes).map(([key, value]) => [
|
||||
key, typeof value === "string" ? t(value) : t(value.message, value.params),
|
||||
]));
|
||||
return cloneElement(children, props);
|
||||
// Children.toArray resolves lazy children streamed across the server/client boundary.
|
||||
const child = Children.only(Children.toArray(children)[0]) as ReactElement;
|
||||
return cloneElement(child, props);
|
||||
}
|
||||
|
||||
export function MetricText({ value }: { value: string }) {
|
||||
|
||||
@@ -173,6 +173,7 @@ export function ArtifactImageExport({
|
||||
</div>
|
||||
) : null}
|
||||
<GuideImageExport
|
||||
allowWithoutNote
|
||||
fileName={fileName}
|
||||
width={exportWidth}
|
||||
height={exportHeight}
|
||||
|
||||
@@ -27,6 +27,7 @@ export function GuideImageExport({
|
||||
disabled = false,
|
||||
captureClassName,
|
||||
buttonLabel,
|
||||
allowWithoutNote = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
fileName: string;
|
||||
@@ -36,6 +37,7 @@ export function GuideImageExport({
|
||||
disabled?: boolean;
|
||||
captureClassName?: string;
|
||||
buttonLabel?: string;
|
||||
allowWithoutNote?: boolean;
|
||||
}) {
|
||||
const { e, locale } = useTranslation();
|
||||
const captureRef = useRef<HTMLDivElement>(null);
|
||||
@@ -111,7 +113,7 @@ export function GuideImageExport({
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(["with-notes", "without-notes"] as const).map((mode) => (
|
||||
{(["with-notes", ...(allowWithoutNote ? ["without-notes"] as const : [])] as const).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
type="button"
|
||||
|
||||
@@ -31,7 +31,7 @@ export function SiteHeader({ editHref = "/admin", guideId }: { editHref?: string
|
||||
className="mr-auto flex min-h-11 items-center gap-3 rounded-lg font-heading font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Image src="/icon.png" alt="" width={36} height={36} className="size-9 object-contain" />
|
||||
<span className="hidden sm:inline">{exportText("watermark")}</span>
|
||||
<span className="hidden sm:inline">Buzz Guide</span>
|
||||
</Link>
|
||||
<LanguageDropdown />
|
||||
<nav aria-label="Public navigation" className="flex items-center gap-1">
|
||||
|
||||
@@ -1102,6 +1102,7 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
|
||||
<div key={group.id} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<GuideImageExport
|
||||
allowWithoutNote
|
||||
fileName={`${guide.name}-Constellations${groups.length > 1 ? `-${group.name || `Condition-${groupIndex + 1}`}` : ""}.png`}
|
||||
width={1500}
|
||||
height={1500}
|
||||
|
||||
@@ -140,6 +140,7 @@ export function WeaponImageExport({
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{metricOptions.map(([metric, label]) => (
|
||||
<GuideImageExport
|
||||
allowWithoutNote
|
||||
key={metric}
|
||||
fileName={`${baseName}-${label}.png`}
|
||||
width={1200}
|
||||
|
||||
@@ -51,14 +51,21 @@ describe("export translations", () => {
|
||||
|
||||
describe("image export controls", () => {
|
||||
it("always offers both export modes even without notes", () => {
|
||||
const html = renderToStaticMarkup(<LocaleBoundary locale="en"><GuideImageExport fileName="guide.png">No notes</GuideImageExport></LocaleBoundary>);
|
||||
const html = renderToStaticMarkup(<LocaleBoundary locale="en"><GuideImageExport allowWithoutNote fileName="guide.png">No notes</GuideImageExport></LocaleBoundary>);
|
||||
expect(html).toContain("Export image");
|
||||
expect(html).toContain("Export without note");
|
||||
expect(html.match(/<button\b/gu)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("only offers normal export unless note omission is enabled", () => {
|
||||
const html = renderToStaticMarkup(<LocaleBoundary locale="en"><GuideImageExport fileName="overview.png">Overview</GuideImageExport></LocaleBoundary>);
|
||||
expect(html.match(/<button\b/gu)).toHaveLength(1);
|
||||
expect(html).toContain("Export image");
|
||||
expect(html).not.toContain("Export without note");
|
||||
});
|
||||
|
||||
it("disables both modes when an export selection is empty", () => {
|
||||
const html = renderToStaticMarkup(<GuideImageExport fileName="guide.png" disabled>No sections</GuideImageExport>);
|
||||
const html = renderToStaticMarkup(<GuideImageExport allowWithoutNote fileName="guide.png" disabled>No sections</GuideImageExport>);
|
||||
expect(html.match(/\sdisabled=""/gu)).toHaveLength(2);
|
||||
});
|
||||
|
||||
|
||||
+17
-1
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import type { ReactElement } from "react";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
@@ -81,6 +82,21 @@ describe("interface translations", () => {
|
||||
expect(html).toContain("Talent Priority");
|
||||
});
|
||||
|
||||
it("localises a lazy child streamed from a server component", () => {
|
||||
const streamedChild = {
|
||||
$$typeof: Symbol.for("react.lazy"),
|
||||
_payload: <Link href="/">Home</Link>,
|
||||
_init: (element: ReactElement) => element,
|
||||
} as unknown as ReactElement;
|
||||
const html = renderToStaticMarkup(
|
||||
<LocaleBoundary locale="en">
|
||||
<Localized attributes={{ "aria-label": "กลับหน้ารวม" }}>{streamedChild}</Localized>
|
||||
</LocaleBoundary>,
|
||||
);
|
||||
expect(html).toContain('aria-label="Back to all guides"');
|
||||
expect(html).toContain("Home");
|
||||
});
|
||||
|
||||
it("preserves custom content while translating only generated headings and conditions", () => {
|
||||
const html = renderToStaticMarkup(<LocaleBoundary locale="en"><input defaultValue="ชื่อภาษาไทย" /><ConditionLabel name="เงื่อนไขของฉัน" index={1} /><ConditionLabel name={null} index={2} /><SectionTitle slug="custom" fallback="หัวข้อของฉัน" /></LocaleBoundary>);
|
||||
expect(html).toContain('value="ชื่อภาษาไทย"');
|
||||
@@ -99,7 +115,7 @@ describe("interface translations", () => {
|
||||
});
|
||||
|
||||
it.each(["th", "en"] as const)("offers both export modes in %s even with no notes", (locale) => {
|
||||
const html = renderToStaticMarkup(<LocaleBoundary locale={locale}><GuideImageExport fileName="test.png">No notes</GuideImageExport></LocaleBoundary>);
|
||||
const html = renderToStaticMarkup(<LocaleBoundary locale={locale}><GuideImageExport allowWithoutNote fileName="test.png">No notes</GuideImageExport></LocaleBoundary>);
|
||||
expect(html).toContain(locale === "en" ? "Export without note" : "ส่งออกภาพโดยไม่รวมโน้ต");
|
||||
expect(html.match(/<button\b/gu)).toHaveLength(2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user