Merge pull request 'bulk importer' (#4) from bulk-import into main

Reviewed-on: http://gitea.lan:3000/robbond/rc-compat/pulls/4
This commit is contained in:
2025-11-25 09:51:19 +00:00
2 changed files with 293 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
"use client";
import { useState } from "react";
import { supabaseBrowser } from "@/lib/supabaseClient";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
// Optional helper: guess manufacturer from part ID prefix
function guessManufacturerFromSku(id = "") {
if (id.startsWith("TAM-")) return "Tamiya";
if (id.startsWith("TRA-")) return "Traxxas";
return null;
}
export default function ImportPage() {
const [jsonText, setJsonText] = useState("");
const [parsed, setParsed] = useState(null);
const [status, setStatus] = useState("");
const [importing, setImporting] = useState(false);
function handleFile(e) {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => setJsonText(reader.result);
reader.readAsText(file);
}
function preview() {
try {
const obj = JSON.parse(jsonText);
setParsed(obj);
setStatus("✅ JSON parsed successfully.");
} catch (err) {
setStatus("❌ Invalid JSON: " + err.message);
}
}
async function runImport() {
if (!parsed) {
setStatus("❌ No parsed JSON to import.");
return;
}
const { models = [], parts = [] } = parsed;
if (!Array.isArray(models) || !Array.isArray(parts)) {
setStatus("❌ JSON must contain `models` and `parts` arrays.");
return;
}
setImporting(true);
setStatus("Importing data…");
const modelSlugToId = {}; // map: slug (id in stub) -> DB uuid
// STEP 1: Insert models
for (const m of models) {
try {
const metadata = {
scale: m.scale || null,
categories: m.categories || [],
generation: m.generation ?? null,
slug: m.id, // keep the original slug
};
const { data, error } = await supabaseBrowser
.from("models")
.insert({
name: m.name,
manufacturer: m.brand || null,
category:
Array.isArray(m.categories) && m.categories.length > 0
? m.categories[0]
: null,
year: m.year || null,
description: null,
metadata,
})
.select()
.single();
if (error) {
setStatus(`❌ Error inserting model "${m.name}": ${error.message}`);
setImporting(false);
return;
}
modelSlugToId[m.id] = data.id;
} catch (err) {
setStatus(`❌ Exception inserting model "${m.name}": ${String(err)}`);
setImporting(false);
return;
}
}
// STEP 2: Insert parts and compatibility
for (const p of parts) {
try {
const metadata = {
upgrade: p.upgrade ?? false,
universalFit: p.universalFit ?? false,
};
const { data: partData, error: partErr } = await supabaseBrowser
.from("parts")
.insert({
name: p.name,
manufacturer: guessManufacturerFromSku(p.id),
category: p.category || null,
sku: p.id,
description: p.notes || null,
metadata,
})
.select()
.single();
if (partErr) {
setStatus(`❌ Error inserting part "${p.name}": ${partErr.message}`);
setImporting(false);
return;
}
// Compatibility from fitsModels
if (Array.isArray(p.fitsModels)) {
for (const slug of p.fitsModels) {
const model_id = modelSlugToId[slug];
if (!model_id) {
// model not found just warn, don't abort
console.warn("Unknown model slug in fitsModels:", slug);
continue;
}
const fit_type = "direct"; // default; could be expanded later
await supabaseBrowser.from("parts_compatible_models").insert({
part_id: partData.id,
model_id,
fit_type,
notes: p.notes || "",
});
}
}
} catch (err) {
setStatus(`❌ Exception inserting part "${p.name}": ${String(err)}`);
setImporting(false);
return;
}
}
setStatus("✅ Import complete!");
setImporting(false);
}
return (
<Card className="max-w-3xl mx-auto">
<CardHeader>
<CardTitle className="text-2xl">Bulk JSON Import (Stub Data)</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* FILE UPLOAD */}
<div>
<Label>Upload JSON File</Label>
<Input type="file" accept=".json" onChange={handleFile} />
</div>
{/* TEXTAREA */}
<div>
<Label>Or paste JSON</Label>
<Textarea
value={jsonText}
onChange={(e) => setJsonText(e.target.value)}
className="min-h-[200px] font-mono text-sm"
/>
</div>
{/* PREVIEW BUTTON */}
<Button onClick={preview} variant="secondary">
Preview JSON
</Button>
{/* PARSED PREVIEW */}
{parsed && (
<pre className="bg-gray-900 text-gray-200 p-4 rounded text-xs overflow-x-auto max-h-64">
{JSON.stringify(parsed, null, 2)}
</pre>
)}
{/* IMPORT BUTTON */}
<Button onClick={runImport} disabled={importing}>
{importing ? "Importing…" : "Import Data"}
</Button>
{/* STATUS */}
{status && (
<div className="p-3 bg-gray-100 border rounded text-sm whitespace-pre-wrap">
{status}
</div>
)}
</CardContent>
</Card>
);
}
+85
View File
@@ -0,0 +1,85 @@
{
"models": [
{
"id": "tamiya-hotshot",
"brand": "Tamiya",
"name": "Hotshot",
"scale": "1/10",
"year": 1985,
"categories": [
"drivetrain",
"suspension",
"chassis",
"shocks"
],
"generation": 1
},
{
"id": "tamiya-supershot",
"brand": "Tamiya",
"name": "Super Shot",
"scale": "1/10",
"year": 1986,
"categories": [
"drivetrain",
"suspension",
"chassis",
"shocks"
],
"generation": 1
},
{
"id": "traxxas-rustler-4x4",
"brand": "Traxxas",
"name": "Rustler 4x4",
"scale": "1/10",
"year": 2018,
"categories": [
"drivetrain",
"suspension",
"electronics",
"steering"
],
"generation": 2
}
],
"parts": [
{
"id": "TAM-198055",
"name": "Hotshot Front Gearbox",
"category": "drivetrain",
"fitsModels": [
"tamiya-hotshot",
"tamiya-supershot"
],
"notes": "Original vintage fit."
},
{
"id": "TAM-430512",
"name": "Super Shot Steering Knuckle",
"category": "steering",
"fitsModels": [
"tamiya-supershot"
]
},
{
"id": "TRA-6755X",
"name": "Traxxas Steel Driveshaft Upgrade",
"category": "drivetrain",
"fitsModels": [
"traxxas-rustler-4x4"
],
"upgrade": true
},
{
"id": "GEN-UNIV-55MM",
"name": "Generic 55mm Shock Set",
"category": "shocks",
"fitsModels": [
"tamiya-hotshot",
"tamiya-supershot"
],
"universalFit": true
}
]
}