feat(import): lego-sets ueber die rebrickable-api uebernehmen

Auf "Neues Set" reicht die Setnummer von der Packung: Name, Jahr,
Teile, Minifiguren und Thema kommen aus der API, das Bild wird lokal
gespeichert und nicht nur verlinkt. Hersteller und Thema werden bei
Bedarf angelegt, ein Oberthema inklusive. Alle Aufrufe laufen
serverseitig; der Browser spricht nie mit rebrickable.com.

Ohne REBRICKABLE_API_KEY bleibt der Import sichtbar, aber abgeschaltet,
mit dem Hinweis, was zu tun ist - von Hand anlegen geht unveraendert.

Rebrickable liefert keine Anleitungsdateien. Statt das zu verschleiern,
legt der Import einen externen Link auf die LEGO-Serviceseite zur
Setnummer an.

Nebenbei: src/env.ts behandelt leere Werte jetzt wie nicht gesetzt - ein
"BASE_URL=" in der .env fuehrte sonst zum Start-Abbruch mit "Invalid
URL" statt zum Standardwert.

14 neue Unit-Tests decken Antwortverarbeitung, Setnummern-Varianten und
alle Fehlerfaelle der API ab (404, 401, 429, Netzwerk).

Refs Phase 2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
unknown
2026-09-07 07:12:22 +02:00
co-authored by Claude Opus 5
parent 52d69e5654
commit 4e3cbb81ce
13 changed files with 813 additions and 6 deletions
@@ -0,0 +1,95 @@
'use client';
import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';
import { Download, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { importSetFromRebrickable } from '@/app/(app)/katalog/import-actions';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Field } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
/**
* One-field Lego import: type the number from the box, get a finished catalog
* entry. Hidden entirely when no API key is configured.
*/
export function RebrickableImport({ enabled }: { enabled: boolean }) {
const router = useRouter();
const [setNumber, setSetNumber] = useState('');
const [pending, startTransition] = useTransition();
if (!enabled) {
return (
<Card className="border-dashed">
<CardHeader>
<CardTitle>Lego-Import</CardTitle>
<CardDescription>
Mit einem Rebrickable-API-Key füllt sich das Formular von selbst: Setnummer eingeben,
und Name, Jahr, Teile, Thema und Bild kommen automatisch. Dafür{' '}
<code className="font-mono text-xs">REBRICKABLE_API_KEY</code> in die{' '}
<code className="font-mono text-xs">.env</code> eintragen und den Container neu
starten.
</CardDescription>
</CardHeader>
</Card>
);
}
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
startTransition(async () => {
const result = await importSetFromRebrickable(setNumber);
if (!result.ok) {
toast.error(result.error);
return;
}
const missing = [
result.data.imported.image ? null : 'Bild',
result.data.imported.theme ? null : 'Thema',
].filter(Boolean);
toast.success(
missing.length === 0
? `${result.data.setNumber} „${result.data.name}“ übernommen.`
: `${result.data.setNumber} übernommen – ohne ${missing.join(' und ')}.`,
);
router.push(`/katalog/${result.data.setId}`);
router.refresh();
});
}
return (
<Card>
<CardHeader>
<CardTitle>Lego-Import</CardTitle>
<CardDescription>
Setnummer von der Packung eintippen – Name, Jahr, Teile, Minifiguren, Thema und Bild
werden übernommen. Das Bild wird lokal gespeichert.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={submit} className="flex items-end gap-2">
<Field label="Setnummer" htmlFor="import-number" className="flex-1">
<Input
id="import-number"
value={setNumber}
onChange={(event) => setSetNumber(event.target.value)}
placeholder="60398"
inputMode="numeric"
maxLength={40}
className="font-mono"
/>
</Field>
<Button type="submit" size="lg" disabled={pending || setNumber.trim().length === 0}>
{pending ? <Loader2 className="animate-spin" /> : <Download />}
Holen
</Button>
</form>
</CardContent>
</Card>
);
}