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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user