diff --git a/.vscode/launch.json b/.vscode/launch.json index 8f9df64..f961904 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "command": "npm run dev" }, {"name": "Launch Chrome against localhost", - "type": "pwa-chrome", + "type": "chrome", "request": "launch", "url": "http://localhost:3000", "webRoot": "${workspaceFolder}" diff --git a/dynamic-route-healthcheck.sh b/bash-setup-scripts/dynamic-route-healthcheck.sh similarity index 100% rename from dynamic-route-healthcheck.sh rename to bash-setup-scripts/dynamic-route-healthcheck.sh diff --git a/fix-api-dynamic-routes.sh b/bash-setup-scripts/fix-api-dynamic-routes.sh similarity index 100% rename from fix-api-dynamic-routes.sh rename to bash-setup-scripts/fix-api-dynamic-routes.sh diff --git a/patch-dynamic-routes.sh b/bash-setup-scripts/patch-dynamic-routes.sh similarity index 100% rename from patch-dynamic-routes.sh rename to bash-setup-scripts/patch-dynamic-routes.sh diff --git a/patch-scss-imports.sh b/bash-setup-scripts/patch-scss-imports.sh similarity index 100% rename from patch-scss-imports.sh rename to bash-setup-scripts/patch-scss-imports.sh diff --git a/patch-tailwind-reference.sh b/bash-setup-scripts/patch-tailwind-reference.sh similarity index 100% rename from patch-tailwind-reference.sh rename to bash-setup-scripts/patch-tailwind-reference.sh diff --git a/setup-api-layer.sh b/bash-setup-scripts/setup-api-layer.sh similarity index 100% rename from setup-api-layer.sh rename to bash-setup-scripts/setup-api-layer.sh diff --git a/bash-setup-scripts/setup-compatibility-option.sh b/bash-setup-scripts/setup-compatibility-option.sh new file mode 100755 index 0000000..c36fab5 --- /dev/null +++ b/bash-setup-scripts/setup-compatibility-option.sh @@ -0,0 +1,259 @@ +#!/bin/zsh + +echo "🛠 Setting up Compatibility Explorer (Option A)..." + +COMPAT_DIR="src/app/compatibility" +mkdir -p "$COMPAT_DIR" + +cat > "$COMPAT_DIR/page.js" << 'EOF' +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { CATEGORIES } from "@/lib/data/categories"; + +function ModeCard({ title, emoji, description, children }) { + return ( +
+
+
{emoji}
+
+

{title}

+

{description}

+
+
+
{children}
+
+ ); +} + +export default function CompatibilityPage() { + const [models, setModels] = useState([]); + const [parts, setParts] = useState([]); + const [loading, setLoading] = useState(true); + + const [modelForParts, setModelForParts] = useState(""); + const [partForModels, setPartForModels] = useState(""); + const [modelA, setModelA] = useState(""); + const [modelB, setModelB] = useState(""); + const [categoryModel, setCategoryModel] = useState(""); + const [category, setCategory] = useState(""); + + const router = useRouter(); + + useEffect(() => { + async function load() { + try { + const [modelsRes, partsRes] = await Promise.all([ + fetch("/api/models"), + fetch("/api/parts"), + ]); + + const modelsJson = modelsRes.ok ? await modelsRes.json() : []; + const partsJson = partsRes.ok ? await partsRes.json() : []; + + setModels(modelsJson || []); + setParts(partsJson || []); + } catch (e) { + console.error("Failed to load models/parts", e); + } finally { + setLoading(false); + } + } + + load(); + }, []); + + function handleGoToModelCompatibility() { + if (!modelForParts) return; + router.push(`/model/${modelForParts}`); + } + + function handleGoToPartCompatibility() { + if (!partForModels) return; + router.push(`/part/${partForModels}`); + } + + function handleExploreCategory() { + if (!categoryModel || !category) return; + router.push(`/category/${categoryModel}/${category}`); + } + + function handleCompareModels() { + if (!modelA || !modelB) return; + // Stubbed for now to avoid broken routes + alert("Model comparison view coming soon! (Hotshot vs Supershot, etc.)"); + } + + return ( +
+ {/* Breadcrumb-ish header */} + + + {/* Page Title */} +
+

Compatibility Explorer

+

+ Discover cross-compatibility between RC models and parts. Use one of the + tools below to explore fitment, upgrades, and shared components. +

+
+ + {loading && ( +

Loading models and parts…

+ )} + + {/* Mode Selector Grid */} + {!loading && ( +
+ {/* Mode 1: Model → Compatible Parts */} + + + + + + {/* Mode 2: Part → Compatible Models */} + + + + + + {/* Mode 3: Compare Two Models (stubbed) */} + +
+ + +
+ +
+ + {/* Mode 4: Explore Categories */} + +
+ + + +
+ +
+
+ )} +
+ ); +} +EOF + +echo "🎉 Compatibility Explorer (Option A) page created at src/app/compatibility/page.js" diff --git a/setup-data-layer.sh b/bash-setup-scripts/setup-data-layer.sh similarity index 100% rename from setup-data-layer.sh rename to bash-setup-scripts/setup-data-layer.sh diff --git a/setup-models-browser.sh b/bash-setup-scripts/setup-models-browser.sh similarity index 100% rename from setup-models-browser.sh rename to bash-setup-scripts/setup-models-browser.sh diff --git a/setup-ui-integration.sh b/bash-setup-scripts/setup-ui-integration.sh similarity index 100% rename from setup-ui-integration.sh rename to bash-setup-scripts/setup-ui-integration.sh diff --git a/setup-ui-polish.sh b/bash-setup-scripts/setup-ui-polish.sh similarity index 100% rename from setup-ui-polish.sh rename to bash-setup-scripts/setup-ui-polish.sh diff --git a/setuprc.sh b/bash-setup-scripts/setuprc.sh similarity index 100% rename from setuprc.sh rename to bash-setup-scripts/setuprc.sh diff --git a/generate_admin_ui.sh b/generate_admin_ui.sh new file mode 100755 index 0000000..684750b --- /dev/null +++ b/generate_admin_ui.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash + +echo "🚀 Generating Admin UI (Next.js App Router, JS, Tailwind)..." + +### Root paths +APP_DIR="src/app/admin" +COMP_DIR="src/components/admin" +LIB_DIR="lib" + +# Create directories +mkdir -p "$APP_DIR" +mkdir -p "$APP_DIR/models/new" +mkdir -p "$APP_DIR/models/[id]" +mkdir -p "$APP_DIR/parts/new" +mkdir -p "$APP_DIR/parts/[id]" +mkdir -p "$COMP_DIR" +mkdir -p "$LIB_DIR" + +############################################ +# 1. ADMIN LAYOUT +############################################ +cat <<'EOF' > "$APP_DIR/layout.js" +import React from "react"; +import Link from "next/link"; +import "../globals.css"; + +export default function AdminLayout({ children }) { + return ( +
+ + +
{children}
+
+ ); +} +EOF + +############################################ +# 2. ADMIN HOME PAGE +############################################ +cat <<'EOF' > "$APP_DIR/page.js" +import { supabaseServer } from "@/lib/supabaseServer"; + +export default async function AdminHome() { + const { data: models } = await supabaseServer.from("models").select("*"); + const { data: parts } = await supabaseServer.from("parts").select("*"); + const { data: compat } = await supabaseServer.from("parts_compatible_models").select("*"); + + return ( +
+

Admin Dashboard

+ +
+
+

Models

+

{models?.length ?? 0}

+
+ +
+

Parts

+

{parts?.length ?? 0}

+
+ +
+

Compatibility

+

{compat?.length ?? 0}

+
+
+
+ ); +} +EOF + +############################################ +# 3. MODELS LIST PAGE +############################################ +cat <<'EOF' > "$APP_DIR/models/page.js" +import Link from "next/link"; +import { supabaseServer } from "@/lib/supabaseServer"; + +export default async function ModelsPage() { + const { data: models } = await supabaseServer + .from("models") + .select("*") + .order("name"); + + return ( +
+
+

Models

+ + + Add Model + +
+ +
+ {models?.map((m) => ( + +
{m.name}
+
{m.brand}
+ + ))} +
+
+ ); +} +EOF + +############################################ +# 4. NEW MODEL PAGE +############################################ +cat <<'EOF' > "$APP_DIR/models/new/page.js" +"use client"; + +import { useState } from "react"; +import { supabaseBrowser } from "@/lib/supabaseClient"; +import { useRouter } from "next/navigation"; + +export default function NewModelPage() { + const [form, setForm] = useState({ + name: "", + brand: "", + scale: "", + chassis_type: "", + notes: "", + }); + + const router = useRouter(); + + async function save() { + const { error } = await supabaseBrowser.from("models").insert(form); + if (!error) router.push("/admin/models"); + } + + return ( +
+

Add New Model

+ +
+ {Object.entries(form).map(([key, value]) => ( +
+ + setForm({ ...form, [key]: e.target.value })} + /> +
+ ))} + + +
+
+ ); +} +EOF + +############################################ +# 5. MODEL DETAIL PAGE +############################################ +cat <<'EOF' > "$APP_DIR/models/[id]/page.js" +import Link from "next/link"; +import { supabaseServer } from "@/lib/supabaseServer"; + +export default async function ModelDetail({ params }) { + const { id } = params; + + const { data: model } = await supabaseServer + .from("models") + .select("*") + .eq("id", id) + .single(); + + const { data: compat } = await supabaseServer + .from("parts_compatible_models") + .select("*, parts(*)") + .eq("model_id", id); + + return ( +
+

{model?.name}

+ +
+
Brand: {model?.brand}
+
Scale: {model?.scale}
+
Chassis: {model?.chassis_type}
+
+ +

Compatible Parts

+ + + + Add Compatible Part + + +
+ {compat?.map((c) => ( +
+
+
{c.parts.name}
+
{c.fit_type}
+
+ + View Part + +
+ ))} +
+
+ ); +} +EOF + +############################################ +# PARTS PAGES (STUBS) +############################################ + +cat <<'EOF' > "$APP_DIR/parts/page.js" +export default function PartsPage() { + return ( +
+

Parts (UI Coming Next)

+

List, edit, and link parts here.

+
+ ); +} +EOF + +cat <<'EOF' > "$APP_DIR/parts/new/page.js" +export default function NewPartPage() { + return ( +
+

Add Part (UI Coming Next)

+
+ ); +} +EOF + +############################################ +# SUPABASE CLIENTS +############################################ + +cat <<'EOF' > "$LIB_DIR/supabaseClient.js" +import { createClient } from "@supabase/supabase-js"; + +export const supabaseBrowser = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY +); +EOF + +cat <<'EOF' > "$LIB_DIR/supabaseServer.js" +import { createClient } from "@supabase/supabase-js"; + +export const supabaseServer = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY +); +EOF + +echo "🎉 Admin UI scaffolding complete!" diff --git a/package-lock.json b/package-lock.json index ba0b8ab..9f1d55e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "ISC", "dependencies": { + "@supabase/supabase-js": "^2.84.0", "fs": "^0.0.1-security", "next": "16.0.3", "path": "^0.12.7", @@ -1464,6 +1465,79 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "node_modules/@supabase/auth-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.84.0.tgz", + "integrity": "sha512-J6XKbqqg1HQPMfYkAT9BrC8anPpAiifl7qoVLsYhQq5B/dnu/lxab1pabnxtJEsvYG5rwI5HEVEGXMjoQ6Wz2Q==", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.84.0.tgz", + "integrity": "sha512-2oY5QBV4py/s64zMlhPEz+4RTdlwxzmfhM1k2xftD2v1DruRZKfoe7Yn9DCz1VondxX8evcvpc2udEIGzHI+VA==", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.84.0.tgz", + "integrity": "sha512-oplc/3jfJeVW4F0J8wqywHkjIZvOVHtqzF0RESijepDAv5Dn/LThlGW1ftysoP4+PXVIrnghAbzPHo88fNomPQ==", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.84.0.tgz", + "integrity": "sha512-ThqjxiCwWiZAroHnYPmnNl6tZk6jxGcG2a7Hp/3kcolPcMj89kWjUTA3cHmhdIWYsP84fHp8MAQjYWMLf7HEUg==", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.84.0.tgz", + "integrity": "sha512-vXvAJ1euCuhryOhC6j60dG8ky+lk0V06ubNo+CbhuoUv+sl39PyY0lc+k+qpQhTk/VcI6SiM0OECLN83+nyJ5A==", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.84.0.tgz", + "integrity": "sha512-byMqYBvb91sx2jcZsdp0qLpmd4Dioe80e4OU/UexXftCkpTcgrkoENXHf5dO8FCSai8SgNeq16BKg10QiDI6xg==", + "dependencies": { + "@supabase/auth-js": "2.84.0", + "@supabase/functions-js": "2.84.0", + "@supabase/postgrest-js": "2.84.0", + "@supabase/realtime-js": "2.84.0", + "@supabase/storage-js": "2.84.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1756,6 +1830,27 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.47.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", @@ -6350,6 +6445,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -6540,6 +6640,26 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index a65c4ac..2e23bf7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@supabase/supabase-js": "^2.84.0", "fs": "^0.0.1-security", "next": "16.0.3", "path": "^0.12.7", diff --git a/scripts/migrateParts.js b/scripts/migrateParts.js deleted file mode 100644 index 74f0947..0000000 --- a/scripts/migrateParts.js +++ /dev/null @@ -1,12 +0,0 @@ -import fs from "fs"; -import path from "path"; - -const PARTS_PATH = path.join(process.cwd(), "src/lib/data/parts.js"); - -let content = fs.readFileSync(PARTS_PATH, "utf8"); - -content = content.replace(/fitsModels/g, "compatibleModels"); - -fs.writeFileSync(PARTS_PATH, content); - -console.log("✔ Migration complete: fitsModels → compatibleModels"); diff --git a/src/app/admin/layout.js b/src/app/admin/layout.js new file mode 100644 index 0000000..9793d25 --- /dev/null +++ b/src/app/admin/layout.js @@ -0,0 +1,33 @@ +import React from "react"; +import Link from "next/link"; +import "../globals.css"; + +export default function AdminLayout({ children }) { + return ( +
+ + +
{children}
+
+ ); +} diff --git a/src/app/admin/models/[id]/page.js b/src/app/admin/models/[id]/page.js new file mode 100644 index 0000000..e11ab34 --- /dev/null +++ b/src/app/admin/models/[id]/page.js @@ -0,0 +1,55 @@ +import Link from "next/link"; +import { supabaseServer } from "@/lib/supabaseServer"; + +export default async function ModelDetail({ params }) { + const { id } = params; + + const { data: model } = await supabaseServer + .from("models") + .select("*") + .eq("id", id) + .single(); + + const { data: compat } = await supabaseServer + .from("parts_compatible_models") + .select("*, parts(*)") + .eq("model_id", id); + + return ( +
+

{model?.name}

+ +
+
Brand: {model?.brand}
+
Scale: {model?.scale}
+
Chassis: {model?.chassis_type}
+
+ +

Compatible Parts

+ + + + Add Compatible Part + + +
+ {compat?.map((c) => ( +
+
+
{c.parts.name}
+
{c.fit_type}
+
+ + View Part + +
+ ))} +
+
+ ); +} diff --git a/src/app/admin/models/new/page.js b/src/app/admin/models/new/page.js new file mode 100644 index 0000000..6982cd8 --- /dev/null +++ b/src/app/admin/models/new/page.js @@ -0,0 +1,48 @@ +"use client"; + +import { useState } from "react"; +import { supabaseBrowser } from "@/lib/supabaseClient"; +import { useRouter } from "next/navigation"; + +export default function NewModelPage() { + const [form, setForm] = useState({ + name: "", + brand: "", + scale: "", + chassis_type: "", + notes: "", + }); + + const router = useRouter(); + + async function save() { + const { error } = await supabaseBrowser.from("models").insert(form); + if (!error) router.push("/admin/models"); + } + + return ( +
+

Add New Model

+ +
+ {Object.entries(form).map(([key, value]) => ( +
+ + setForm({ ...form, [key]: e.target.value })} + /> +
+ ))} + + +
+
+ ); +} diff --git a/src/app/admin/models/page.js b/src/app/admin/models/page.js new file mode 100644 index 0000000..4f963f4 --- /dev/null +++ b/src/app/admin/models/page.js @@ -0,0 +1,43 @@ +import Link from "next/link"; +import { supabaseServer } from "@/lib/supabaseServer"; + +export default async function ModelsPage() { + const { data: models } = await supabaseServer + .from("models") + .select("*") + .order("name"); + + return ( +
+
+

Models

+ + + Add Model + +
+ + {(!models || models.length === 0) && ( +

+ No models found yet. Click “Add Model” to create your + first one. +

+ )} + +
+ {models?.map((m) => ( + +
{m.name}
+
{m.brand}
+ + ))} +
+
+ ); +} diff --git a/src/app/admin/page.js b/src/app/admin/page.js new file mode 100644 index 0000000..cd76b23 --- /dev/null +++ b/src/app/admin/page.js @@ -0,0 +1,32 @@ +import { supabaseServer } from "@/lib/supabaseServer"; + +export default async function AdminHome() { + const { data: models } = await supabaseServer.from("models").select("*"); + const { data: parts } = await supabaseServer.from("parts").select("*"); + const { data: compat } = await supabaseServer + .from("parts_compatible_models") + .select("*"); + + return ( +
+

Admin Dashboard

+ +
+
+

Models

+

{models?.length ?? 0}

+
+ +
+

Parts

+

{parts?.length ?? 0}

+
+ +
+

Compatibility

+

{compat?.length ?? 0}

+
+
+
+ ); +} diff --git a/src/app/admin/parts/[id]/page.js b/src/app/admin/parts/[id]/page.js new file mode 100644 index 0000000..fa115af --- /dev/null +++ b/src/app/admin/parts/[id]/page.js @@ -0,0 +1,221 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { supabaseBrowser } from "@/lib/supabaseClient"; + +export default function PartDetailPage({ params }) { + const { id } = params; + const router = useRouter(); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [deleting, setDeleting] = useState(false); + const [errorMsg, setErrorMsg] = useState(""); + + const [form, setForm] = useState({ + name: "", + manufacturer: "", + sku: "", + category: "", + description: "", + metadata: "", + }); + + useEffect(() => { + async function fetchPart() { + setLoading(true); + setErrorMsg(""); + + const { data, error } = await supabaseBrowser + .from("parts") + .select("*") + .eq("id", id) + .single(); + + if (error) { + console.error(error); + setErrorMsg("Failed to load part."); + setLoading(false); + return; + } + + setForm({ + name: data.name || "", + manufacturer: data.manufacturer || "", + sku: data.sku || "", + category: data.category || "", + description: data.description || "", + metadata: data.metadata ? JSON.stringify(data.metadata, null, 2) : "", + }); + + setLoading(false); + } + + fetchPart(); + }, [id]); + + function updateField(field, value) { + setForm((prev) => ({ ...prev, [field]: value })); + } + + async function handleSave() { + setSaving(true); + setErrorMsg(""); + + let metadata = null; + if (form.metadata.trim()) { + try { + metadata = JSON.parse(form.metadata); + } catch (err) { + setSaving(false); + setErrorMsg("Metadata must be valid JSON."); + return; + } + } + + const payload = { + name: form.name.trim(), + manufacturer: form.manufacturer || null, + sku: form.sku || null, + category: form.category || null, + description: form.description || null, + metadata, + }; + + const { error } = await supabaseBrowser + .from("parts") + .update(payload) + .eq("id", id); + + if (error) { + console.error(error); + setErrorMsg(error.message || "Failed to save changes."); + setSaving(false); + return; + } + + setSaving(false); + } + + async function handleDelete() { + const ok = window.confirm( + "Are you sure you want to delete this part? This cannot be undone." + ); + if (!ok) return; + + setDeleting(true); + setErrorMsg(""); + + const { error } = await supabaseBrowser.from("parts").delete().eq("id", id); + + if (error) { + console.error(error); + setErrorMsg(error.message || "Failed to delete part."); + setDeleting(false); + return; + } + + router.push("/admin/parts"); + } + + if (loading) { + return

Loading part...

; + } + + return ( +
+

Edit Part

+ +
+
+ + updateField("name", e.target.value)} + /> +
+ +
+
+ + updateField("manufacturer", e.target.value)} + /> +
+ +
+ + updateField("sku", e.target.value)} + /> +
+ +
+ + updateField("category", e.target.value)} + /> +
+
+ +
+ +