feat(web): shared sv/en LabelEditor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 09:14:16 +02:00
parent 6afc358334
commit 8d2323ed95
5 changed files with 88 additions and 2 deletions
+37
View File
@@ -0,0 +1,37 @@
import { useState } from "react";
import { expect, test } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderApp } from "../test/render";
import { LabelEditor } from "./label-editor";
import type { components } from "../api/schema";
type LabelInput = components["schemas"]["LabelInput"];
function Harness({ onChange }: { onChange: (v: LabelInput[]) => void }) {
const [value, setValue] = useState<LabelInput[]>([]);
return (
<LabelEditor
value={value}
onChange={(v) => {
setValue(v);
onChange(v);
}}
/>
);
}
test("typing EN and SV emits both labels; empty langs are omitted", async () => {
const seen: LabelInput[][] = [];
renderApp(<Harness onChange={(v) => seen.push(v)} />);
await userEvent.type(screen.getByLabelText(/label \(en\)/i), "Bronze");
await userEvent.type(screen.getByLabelText(/label \(sv\)/i), "Brons");
const last = seen.at(-1)!;
expect(last).toEqual(
expect.arrayContaining([
{ lang: "en", label: "Bronze" },
{ lang: "sv", label: "Brons" },
]),
);
expect(seen.some((v) => v.length === 1 && v[0].lang === "en")).toBe(true);
});
+47
View File
@@ -0,0 +1,47 @@
import { useTranslation } from "react-i18next";
import type { components } from "../api/schema";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
type LabelInput = components["schemas"]["LabelInput"];
/** Controlled sv/en label editor. Emits LabelInput[] with only the non-empty langs. */
export function LabelEditor({
value,
onChange,
}: {
value: LabelInput[];
onChange: (labels: LabelInput[]) => void;
}) {
const { t } = useTranslation();
const valueFor = (lang: string) => value.find((l) => l.lang === lang)?.label ?? "";
const set = (lang: string, label: string) => {
const others = value.filter((l) => l.lang !== lang);
onChange(label ? [...others, { lang, label }] : others);
};
return (
<div className="space-y-2">
<div className="space-y-1">
<Label htmlFor="label-en">{t("labels.en")}</Label>
<Input
id="label-en"
value={valueFor("en")}
onChange={(e) => set("en", e.target.value)}
/>
</div>
<div className="space-y-1">
<Label htmlFor="label-sv">{t("labels.sv")}</Label>
<Input
id="label-sv"
value={valueFor("sv")}
onChange={(e) => set("sv", e.target.value)}
/>
</div>
</div>
);
}