209 lines
5.8 KiB
JavaScript
209 lines
5.8 KiB
JavaScript
"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>
|
||
);
|
||
}
|