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