added in admin pages to add models and parts

This commit is contained in:
2025-11-24 18:06:26 +00:00
parent f8bb195159
commit 2682308015
30 changed files with 1569 additions and 57 deletions
+107
View File
@@ -0,0 +1,107 @@
#!/bin/zsh
echo "🔎 Running Next.js Dynamic Route Health Check..."
echo ""
DYNAMIC_PAGES=$(find src/app -type f -path "*/\[*\]/page.js")
RED=$(tput setaf 1)
YELLOW=$(tput setaf 3)
GREEN=$(tput setaf 2)
RESET=$(tput sgr0)
check_pass() {
echo "${GREEN}✔ PASS${RESET} $1"
}
check_warn() {
echo "${YELLOW}⚠ WARN${RESET} $1"
}
check_fail() {
echo "${RED}✖ FAIL${RESET} $1"
}
for FILE in $DYNAMIC_PAGES; do
echo "=========================================="
echo "📄 Checking: $FILE"
FOLDER=$(basename $(dirname "$FILE"))
PARAM_NAME=${FOLDER//[\[\]]/}
CONTENT=$(cat "$FILE")
echo "➡ Route param is: ${PARAM_NAME}"
# 1. Check for export default async
if echo "$CONTENT" | grep -q "export default async function"; then
check_pass "Has async default export"
else
check_fail "Missing 'export default async function'"
fi
# 2. Check for await params
if echo "$CONTENT" | grep -q "await params"; then
check_pass "'await params' found"
else
check_fail "Missing 'await params' — required in Next.js 15+"
fi
# 3. Check if extracted param name is correct
if echo "$CONTENT" | grep -q "const { $PARAM_NAME } = await params"; then
check_pass "Correct param extraction: { $PARAM_NAME }"
else
check_warn "Param '$PARAM_NAME' may not be extracted with 'await params'"
fi
# 4. Check for leftover bad usage: params.paramName
if echo "$CONTENT" | grep -q "params\.$PARAM_NAME"; then
check_fail "Found old syntax 'params.$PARAM_NAME' — must be removed!"
else
check_pass "No old 'params.$PARAM_NAME' references"
fi
# 5. Check that param is used somewhere
if echo "$CONTENT" | grep -q "$PARAM_NAME"; then
check_pass "Param is referenced in file"
else
check_warn "Param '$PARAM_NAME' isn't used — this may be incorrect"
fi
# 6. Validate imports
IMPORT_ERRORS=0
while IFS= read -r line; do
if [[ "$line" == import* ]]; then
PATH=$(echo "$line" | sed -n 's/.*from "\(.*\)".*/\1/p')
if [[ "$PATH" == @/* ]]; then
# Attempt resolution
RESOLVED="src${PATH#@}"
RESOLVED="${RESOLVED%.js}.js"
if [ ! -f "$RESOLVED" ] && [ ! -f "${RESOLVED%.js}.jsx" ] && [ ! -f "${RESOLVED%.js}.ts" ] && [ ! -f "${RESOLVED%.js}.tsx" ]; then
check_warn "Import may be broken: $line"
IMPORT_ERRORS=1
fi
fi
fi
done <<< "$(grep "^import" "$FILE")"
if [[ $IMPORT_ERRORS -eq 0 ]]; then
check_pass "All imports appear valid"
fi
# 7. Check param folder structure
DIR=$(dirname "$FILE")
if [ ! -d "$DIR" ]; then
check_fail "Missing directory: $DIR (route broken)"
else
check_pass "Route directory exists"
fi
echo ""
done
echo "=========================================="
echo "🏁 Dynamic Route Health Check Complete!"
+73
View File
@@ -0,0 +1,73 @@
#!/bin/zsh
echo "🛠 Starting API Dynamic Route Autofix (Next.js 15+)..."
echo ""
# Find all dynamic API routes like src/app/api/**/[something]/route.js
DYNAMIC_API=$(find src/app/api -type f -path "*/\[*\]/route.js")
RED=$(tput setaf 1)
YELLOW=$(tput setaf 3)
GREEN=$(tput setaf 2)
RESET=$(tput sgr0)
for FILE in $DYNAMIC_API; do
echo "=========================================="
echo "📄 Fixing API route: $FILE"
# Extract folder name: [id], [modelId], [partId], etc.
FOLDER=$(basename $(dirname "$FILE"))
PARAM_NAME=${FOLDER//[\[\]]/} # remove brackets
echo "➡ Dynamic API param detected: ${GREEN}${PARAM_NAME}${RESET}"
# Make backup
cp "$FILE" "$FILE.bak"
CONTENT=$(cat "$FILE")
############################################################
# 1. Fix function signature
############################################################
if echo "$CONTENT" | grep -q "export async function GET(request, { params })"; then
echo " 🔧 Fixing signature..."
sed -i '' "s/export async function GET(request, { params })/export async function GET(request, context) {\n const { $PARAM_NAME } = await context.params;\n/" "$FILE"
elif ! echo "$CONTENT" | grep -q "await context.params"; then
echo " 🔧 Inserting param extraction at top of GET function..."
sed -i '' "s/export async function GET([^)]*) {/export async function GET(request, context) {\n const { $PARAM_NAME } = await context.params;/" "$FILE"
else
echo " ✔ Signature already patched"
fi
############################################################
# 2. Fix incorrect usage of params.PARAM_NAME
############################################################
if echo "$CONTENT" | grep -q "params.$PARAM_NAME"; then
echo " 🔧 Removing old params.$PARAM_NAME usage..."
sed -i '' "s/params\.$PARAM_NAME/$PARAM_NAME/g" "$FILE"
else
echo " ✔ No old params.$PARAM_NAME found"
fi
############################################################
# 3. Validate presence of correct variable usage
############################################################
if grep -q "$PARAM_NAME" "$FILE"; then
echo " ✔ Param $PARAM_NAME is being used correctly"
else
echo " ${YELLOW}⚠ WARN:${RESET} Param '$PARAM_NAME' not found in file. You may need manual review."
fi
############################################################
echo " ${GREEN}✔ API route successfully patched${RESET}"
echo ""
done
echo "=========================================="
echo "🏁 API Dynamic Route Autofix Complete!"
+39
View File
@@ -0,0 +1,39 @@
#!/bin/zsh
echo "🛠 Patching all dynamic Next.js route pages to use 'await params'..."
# Find all dynamic folders: anything like /[paramName]/
DYNAMIC_PAGES=$(find src/app -type f -path "*/\[*\]/page.js")
for FILE in $DYNAMIC_PAGES; do
echo "📄 Patching: $FILE"
# Extract folder name, e.g. [partId]
FOLDER=$(basename $(dirname "$FILE"))
# Remove brackets -> partId
PARAM_NAME=${FOLDER//[\[\]]/}
# Make backup
cp "$FILE" "$FILE.bak"
# Replace any direct use of params.PARAM_NAME
# Ensure the file declares: const { paramName } = await params;
# Only insert if not already patched
if ! grep -q "await params" "$FILE"; then
echo " Inserting param extraction: const { $PARAM_NAME } = await params;"
# Insert param extraction after function signature
sed -i '' "s/export default async function \(.*\)({ params }) {/export default async function \1({ params }) {\n const { $PARAM_NAME } = await params;/" "$FILE"
else
echo " ✔ Already patched."
fi
echo " 🔧 Updating references to params.$PARAM_NAME..."
# Replace params.PARAM_NAME → PARAM_NAME
sed -i '' "s/params\.$PARAM_NAME/$PARAM_NAME/g" "$FILE"
done
echo "🎉 All dynamic route pages patched successfully!"
+42
View File
@@ -0,0 +1,42 @@
#!/bin/zsh
echo "🎨 Depth-aware SCSS variables import patcher starting..."
VARIABLES_FILE="src/app/styles/variables.scss"
if [ ! -f "$VARIABLES_FILE" ]; then
echo "❌ variables.scss not found at: $VARIABLES_FILE"
exit 1
fi
# Normalize absolute path for comparison
VARIABLES_PATH=$(realpath "$VARIABLES_FILE")
# Find all SCSS files under src/app (excluding the variables file itself)
SCSS_FILES=$(find src/app -type f -name "*.scss" ! -path "*variables.scss")
for FILE in $SCSS_FILES; do
echo "🔍 Checking: $FILE"
# Skip if already importing variables
if grep -q 'variables' "$FILE"; then
echo " ✔ Already has variables import"
continue
fi
# Compute relative path from current SCSS file directory to variables.scss
FILE_DIR=$(dirname "$FILE")
REL_PATH=$(realpath --relative-to="$FILE_DIR" "$VARIABLES_PATH")
# Convert absolute to Sass import (remove leading ./ if present)
IMPORT_PATH="${REL_PATH#./}"
echo " Adding import: @import \"$IMPORT_PATH\";"
# Prepend import to file
echo "@import \"$IMPORT_PATH\";" | cat - "$FILE" > "$FILE.tmp"
mv "$FILE.tmp" "$FILE"
done
echo "🎉 Depth-aware SCSS import patching complete!"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/zsh
echo "🩹 Auto-patching SCSS files with @reference \"tailwindcss\"..."
SCSS_FILES=$(find src -type f -name "*.scss")
for FILE in $SCSS_FILES; do
echo "Checking: $FILE"
# Skip if already has @reference
if grep -q '@reference "tailwindcss"' "$FILE"; then
echo " ✔ Already patched"
continue
fi
echo " Adding @reference to $FILE"
# Prepend @reference at top of file
echo '@reference "tailwindcss";' | cat - "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"
done
echo "🎉 All SCSS files have been patched for Tailwind v4!"
+110
View File
@@ -0,0 +1,110 @@
#!/bin/zsh
echo "📡 Creating API Layer for RC Compatibility App..."
BASE="src/app/api"
#############################################
# CREATE DIRECTORY STRUCTURE
#############################################
mkdir -p "$BASE/models"
mkdir -p "$BASE/models/[id]"
mkdir -p "$BASE/parts"
mkdir -p "$BASE/parts/[id]"
mkdir -p "$BASE/compatibility/model/[id]"
mkdir -p "$BASE/compatibility/part/[id]"
#############################################
# MODELS → /api/models
#############################################
cat > "$BASE/models/route.js" << 'EOF'
import { MODELS } from "@/src/lib/data/models";
export async function GET() {
return Response.json(MODELS);
}
EOF
#############################################
# MODEL BY ID → /api/models/[id]
#############################################
cat > "$BASE/models/[id]/route.js" << 'EOF'
import { MODELS } from "@/src/lib/data/models";
export async function GET(request, { params }) {
const model = MODELS.find(m => m.id === params.id);
if (!model) {
return Response.json({ error: "Model not found" }, { status: 404 });
}
return Response.json(model);
}
EOF
#############################################
# PARTS → /api/parts
#############################################
cat > "$BASE/parts/route.js" << 'EOF'
import { PARTS } from "@/src/lib/data/parts";
export async function GET() {
return Response.json(PARTS);
}
EOF
#############################################
# PART BY ID → /api/parts/[id]
#############################################
cat > "$BASE/parts/[id]/route.js" << 'EOF'
import { PARTS } from "@/src/lib/data/parts";
export async function GET(request, { params }) {
const part = PARTS.find(p => p.id === params.id);
if (!part) {
return Response.json({ error: "Part not found" }, { status: 404 });
}
return Response.json(part);
}
EOF
#############################################
# MODEL COMPATIBILITY → /api/compatibility/model/[id]
#############################################
cat > "$BASE/compatibility/model/[id]/route.js" << 'EOF'
import { getCompatibleParts } from "@/src/lib/compatibility/engine";
export async function GET(request, { params }) {
const results = getCompatibleParts(params.id);
if (!results.length) {
return Response.json({ error: "No compatible parts or model not found" }, { status: 404 });
}
return Response.json(results);
}
EOF
#############################################
# PART COMPATIBILITY → /api/compatibility/part/[id]
#############################################
cat > "$BASE/compatibility/part/[id]/route.js" << 'EOF'
import { getCrossCompatibleModels } from "@/src/lib/compatibility/engine";
export async function GET(request, { params }) {
const results = getCrossCompatibleModels(params.id);
if (!results.length) {
return Response.json({ error: "No compatible models or part not found" }, { status: 404 });
}
return Response.json(results);
}
EOF
echo "🎉 API Layer created successfully!"
+259
View File
@@ -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 (
<div className="border rounded-xl p-6 shadow-sm bg-white flex flex-col gap-4">
<div className="flex items-center gap-3">
<div className="text-2xl">{emoji}</div>
<div>
<h2 className="font-semibold text-lg">{title}</h2>
<p className="text-sm text-gray-500">{description}</p>
</div>
</div>
<div className="mt-2 flex flex-col gap-3">{children}</div>
</div>
);
}
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 (
<div className="space-y-10">
{/* Breadcrumb-ish header */}
<nav className="text-sm text-gray-500">
<Link href="/" className="hover:underline text-blue-600">
Home
</Link>{" "}
<span></span>{" "}
<span className="font-semibold text-gray-700">Compatibility Explorer</span>
</nav>
{/* Page Title */}
<div>
<h1 className="text-4xl font-bold mb-2">Compatibility Explorer</h1>
<p className="text-gray-600 max-w-2xl">
Discover cross-compatibility between RC models and parts. Use one of the
tools below to explore fitment, upgrades, and shared components.
</p>
</div>
{loading && (
<p className="text-gray-500">Loading models and parts…</p>
)}
{/* Mode Selector Grid */}
{!loading && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Mode 1: Model → Compatible Parts */}
<ModeCard
title="Model → Compatible Parts"
emoji="🔧"
description="Find every part that fits a specific RC model."
>
<select
className="border rounded-lg px-3 py-2"
value={modelForParts}
onChange={(e) => setModelForParts(e.target.value)}
>
<option value="">Select a model…</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name} ({m.brand})
</option>
))}
</select>
<button
onClick={handleGoToModelCompatibility}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
disabled={!modelForParts}
>
Show Compatible Parts
</button>
</ModeCard>
{/* Mode 2: Part → Compatible Models */}
<ModeCard
title="Part → Compatible Models"
emoji="🧩"
description="See all RC models that this part works with."
>
<select
className="border rounded-lg px-3 py-2"
value={partForModels}
onChange={(e) => setPartForModels(e.target.value)}
>
<option value="">Select a part…</option>
{parts.map((p) => (
<option key={p.id} value={p.id}>
{p.name} ({p.id})
</option>
))}
</select>
<button
onClick={handleGoToPartCompatibility}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
disabled={!partForModels}
>
Show Compatible Models
</button>
</ModeCard>
{/* Mode 3: Compare Two Models (stubbed) */}
<ModeCard
title="Compare Two Models"
emoji="🔍"
description="See shared and unique parts between two models. (Coming soon)"
>
<div className="flex flex-col gap-2">
<select
className="border rounded-lg px-3 py-2"
value={modelA}
onChange={(e) => setModelA(e.target.value)}
>
<option value="">Select Model A…</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name} ({m.brand})
</option>
))}
</select>
<select
className="border rounded-lg px-3 py-2"
value={modelB}
onChange={(e) => setModelB(e.target.value)}
>
<option value="">Select Model B…</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name} ({m.brand})
</option>
))}
</select>
</div>
<button
onClick={handleCompareModels}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg cursor-not-allowed"
disabled
>
Compare (Coming Soon)
</button>
</ModeCard>
{/* Mode 4: Explore Categories */}
<ModeCard
title="Explore by Category"
emoji="📂"
description="Browse shocks, drivetrain, chassis, and more for a model."
>
<div className="flex flex-col gap-2">
<select
className="border rounded-lg px-3 py-2"
value={categoryModel}
onChange={(e) => setCategoryModel(e.target.value)}
>
<option value="">Select a model…</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name} ({m.brand})
</option>
))}
</select>
<select
className="border rounded-lg px-3 py-2"
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="">Select a category…</option>
{CATEGORIES.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
</div>
<button
onClick={handleExploreCategory}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
disabled={!categoryModel || !category}
>
Explore Category
</button>
</ModeCard>
</div>
)}
</div>
);
}
EOF
echo "🎉 Compatibility Explorer (Option A) page created at src/app/compatibility/page.js"
+239
View File
@@ -0,0 +1,239 @@
#!/bin/zsh
echo "📦 Creating RC Compatibility Data Layer + Engine..."
BASE="src/lib"
#############################################
# CREATE DIRECTORY STRUCTURE
#############################################
mkdir -p "$BASE/data"
mkdir -p "$BASE/compatibility"
mkdir -p "$BASE/search"
#############################################
# CREATE DATA FILES
#############################################
# categories.js
cat > "$BASE/data/categories.js" << 'EOF'
export const CATEGORIES = [
"drivetrain",
"suspension",
"chassis",
"shocks",
"electronics",
"steering",
"wheels",
"tires",
"body"
];
EOF
# models.js
cat > "$BASE/data/models.js" << 'EOF'
export const 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,
}
];
EOF
# parts.js
cat > "$BASE/data/parts.js" << 'EOF'
export const 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,
}
];
EOF
#############################################
# COMPATIBILITY UTILITIES
#############################################
cat > "$BASE/compatibility/utils.js" << 'EOF'
export function getModel(modelId, models) {
return models.find(m => m.id === modelId) || null;
}
export function getPart(partId, parts) {
return parts.find(p => p.id === partId) || null;
}
EOF
#############################################
# COMPATIBILITY SCORING SYSTEM
#############################################
cat > "$BASE/compatibility/scoring.js" << 'EOF'
export function scoreCompatibility({ directFit, sameBrand, sameCategory, universal, generationMatch }) {
let score = 0;
if (directFit) score += 100;
if (sameBrand) score += 20;
if (sameCategory) score += 15;
if (generationMatch) score += 10;
if (universal) score += 5;
return Math.min(score, 100);
}
EOF
#############################################
# MAIN COMPATIBILITY ENGINE
#############################################
cat > "$BASE/compatibility/engine.js" << 'EOF'
import { MODELS } from "../data/models";
import { PARTS } from "../data/parts";
import { getModel, getPart } from "./utils";
import { scoreCompatibility } from "./scoring";
// Check model-to-part compatibility
export function isCompatible(modelId, partId) {
const model = getModel(modelId, MODELS);
const part = getPart(partId, PARTS);
if (!model || !part) {
return { compatible: false, reason: "Unknown model or part" };
}
const directFit = part.fitsModels.includes(modelId);
const sameBrand = model.brand && part.brand && part.brand === model.brand;
const sameCategory = model.categories.includes(part.category);
const universal = !!part.universalFit;
const generationMatch = part.generation
? part.generation === model.generation
: false;
const score = scoreCompatibility({
directFit,
sameBrand,
sameCategory,
universal,
generationMatch
});
return {
compatible: score > 0,
score,
details: {
directFit,
sameBrand,
sameCategory,
universal,
generationMatch
}
};
}
// Get all compatible parts for a model
export function getCompatibleParts(modelId) {
return PARTS
.map(part => ({
part,
result: isCompatible(modelId, part.id)
}))
.filter(item => item.result.compatible)
.sort((a, b) => b.result.score - a.result.score);
}
// Get models that fit a part
export function getCrossCompatibleModels(partId) {
return MODELS
.map(model => ({
model,
result: isCompatible(model.id, partId)
}))
.filter(item => item.result.compatible)
.sort((a, b) => b.result.score - a.result.score);
}
// Suggest alternative parts in same category
export function suggestAlternatives(partId) {
const part = getPart(partId, PARTS);
if (!part) return [];
return PARTS.filter(p =>
p.category === part.category && p.id !== partId
);
}
EOF
#############################################
# SEARCH ENGINE
#############################################
cat > "$BASE/search/search.js" << 'EOF'
import { MODELS } from "../data/models";
import { PARTS } from "../data/parts";
export function searchModels(query) {
const q = query.toLowerCase();
return MODELS.filter(m =>
m.name.toLowerCase().includes(q) ||
m.brand.toLowerCase().includes(q)
);
}
export function searchParts(query) {
const q = query.toLowerCase();
return PARTS.filter(p =>
p.name.toLowerCase().includes(q) ||
p.id.toLowerCase().includes(q)
);
}
EOF
echo "🎉 Data layer + compatibility engine generated successfully!"
+104
View File
@@ -0,0 +1,104 @@
#!/bin/zsh
echo "📘 Creating Models Browser Page..."
PAGE="src/app/models"
COMP="src/app/components"
LIB="src/lib/ui"
mkdir -p "$PAGE"
#############################################
# Create models/page.js
#############################################
cat > "$PAGE/page.js" << 'EOF'
"use client";
import { useEffect, useState } from "react";
import ModelCard from "../components/ModelCard";
export default function ModelsPage() {
const [models, setModels] = useState([]);
const [filtered, setFiltered] = useState([]);
const [brand, setBrand] = useState("all");
const [scale, setScale] = useState("all");
useEffect(() => {
async function load() {
const res = await fetch("/api/models", { cache: "no-store" });
const data = await res.json();
setModels(data);
setFiltered(data);
}
load();
}, []);
function filter() {
let result = [...models];
if (brand !== "all") {
result = result.filter(m => m.brand === brand);
}
if (scale !== "all") {
result = result.filter(m => m.scale === scale);
}
setFiltered(result);
}
const brands = [...new Set(models.map(m => m.brand))];
const scales = [...new Set(models.map(m => m.scale))];
return (
<div className="space-y-6">
<h1 className="text-4xl font-bold">All Models</h1>
{/* Filters */}
<div className="flex gap-4 items-end">
<div>
<label className="block mb-1 font-semibold">Brand</label>
<select
className="border p-2 rounded"
value={brand}
onChange={e => setBrand(e.target.value)}
>
<option value="all">All</option>
{brands.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
<div>
<label className="block mb-1 font-semibold">Scale</label>
<select
className="border p-2 rounded"
value={scale}
onChange={e => setScale(e.target.value)}
>
<option value="all">All</option>
{scales.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<button
onClick={filter}
className="px-4 py-2 bg-primary text-white rounded"
>
Apply Filters
</button>
</div>
{/* Model Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filtered.map(model => (
<a key={model.id} href={`/model/${model.id}`} className="block">
<ModelCard model={model} />
</a>
))}
</div>
</div>
);
}
EOF
echo "🎉 Models Browser Page created!"
+300
View File
@@ -0,0 +1,300 @@
#!/bin/zsh
echo "🎨 Setting up UI integration (Next.js App Router)..."
BASE="src/app"
COMP="src/app/components"
UTIL="src/lib/ui"
mkdir -p "$COMP"
mkdir -p "$UTIL"
#############################################
# API CLIENT (simple fetch helpers)
#############################################
cat > "$UTIL/api.js" << 'EOF'
export async function fetchJSON(url) {
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) return null;
return res.json();
}
export function getModel(id) {
return fetchJSON(`/api/models/${id}`);
}
export function getPart(id) {
return fetchJSON(`/api/parts/${id}`);
}
export function getCompatibleParts(modelId) {
return fetchJSON(`/api/compatibility/model/${modelId}`);
}
export function getCompatibleModels(partId) {
return fetchJSON(`/api/compatibility/part/${partId}`);
}
EOF
#############################################
# COMPONENTS
#############################################
# ModelCard.js
cat > "$COMP/ModelCard.js" << 'EOF'
export default function ModelCard({ model }) {
return (
<div className="border rounded p-4 shadow">
<h2 className="font-bold text-lg">{model.name}</h2>
<p>{model.brand} • {model.scale}</p>
</div>
);
}
EOF
# PartCard.js
cat > "$COMP/PartCard.js" << 'EOF'
export default function PartCard({ part }) {
return (
<div className="border rounded p-4 shadow">
<h2 className="font-bold text-lg">{part.name}</h2>
<p className="opacity-70">ID: {part.id}</p>
</div>
);
}
EOF
# SearchBar.js
cat > "$COMP/SearchBar.js" << 'EOF'
"use client";
import { useState } from "react";
export default function SearchBar({ onSearch }) {
const [text, setText] = useState("");
return (
<div className="flex gap-2">
<input
className="border p-2 rounded w-full"
placeholder="Search models or parts..."
value={text}
onChange={e => setText(e.target.value)}
/>
<button
className="px-4 py-2 bg-primary text-white rounded"
onClick={() => onSearch(text)}
>
Go
</button>
</div>
);
}
EOF
#############################################
# UI: Model Page Integration
#############################################
cat > "$BASE/model/[modelId]/page.js" << 'EOF'
import { getModel, getCompatibleParts } from "@/src/lib/ui/api";
import PartCard from "@/src/app/components/PartCard";
export default async function ModelPage({ params }) {
const model = await getModel(params.modelId);
const parts = await getCompatibleParts(params.modelId);
if (!model) return <p>Model not found</p>;
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">{model.name}</h1>
<p className="opacity-70">{model.brand} • {model.scale}</p>
<h2 className="text-xl font-bold mt-6">Compatible Parts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Array.isArray(parts) && parts.length > 0 ? (
parts.map(item => <PartCard key={item.part.id} part={item.part} />)
) : (
<p>No compatible parts.</p>
)}
</div>
</div>
);
}
EOF
#############################################
# UI: Part Page Integration
#############################################
cat > "$BASE/part/[partId]/page.js" << 'EOF'
import { getPart, getCompatibleModels } from "@/src/lib/ui/api";
import ModelCard from "@/src/app/components/ModelCard";
export default async function PartDetailPage({ params }) {
const part = await getPart(params.partId);
const models = await getCompatibleModels(params.partId);
if (!part) return <p>Part not found</p>;
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">{part.name}</h1>
<p className="opacity-70">ID: {part.id}</p>
<h2 className="text-xl font-bold mt-6">Compatible Models</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Array.isArray(models) && models.length > 0 ? (
models.map(item => (
<ModelCard key={item.model.id} model={item.model} />
))
) : (
<p>No compatible models.</p>
)}
</div>
</div>
);
}
EOF
#############################################
# UI: Category Page Integration
#############################################
cat > "$BASE/category/[modelId]/[categoryName]/page.js" << 'EOF'
import { getModel, getCompatibleParts } from "@/src/lib/ui/api";
import PartCard from "@/src/app/components/PartCard";
export default async function CategoryPage({ params }) {
const model = await getModel(params.modelId);
const allParts = await getCompatibleParts(params.modelId);
if (!model) return <p>Model not found</p>;
const parts = allParts
? allParts.filter(p => p.part.category === params.categoryName)
: [];
return (
<div>
<h1 className="text-2xl font-bold">
{params.categoryName} for {model.name}
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
{parts.length ? (
parts.map(p => (
<PartCard key={p.part.id} part={p.part} />
))
) : (
<p>No compatible parts in this category.</p>
)}
</div>
</div>
);
}
EOF
#############################################
# UI: Compatibility Explorer Page
#############################################
cat > "$BASE/compatibility/page.js" << 'EOF'
"use client";
import { useState } from "react";
import { fetchJSON } from "@/src/lib/ui/api";
import ModelCard from "../components/ModelCard";
import PartCard from "../components/PartCard";
export default function CompatibilityPage() {
const [query, setQuery] = useState("");
const [result, setResult] = useState([]);
async function compare() {
const modelData = await fetchJSON(`/api/compatibility/model/${query}`);
const partData = await fetchJSON(`/api/compatibility/part/${query}`);
setResult(modelData || partData || []);
}
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Compatibility Explorer</h1>
<div className="flex gap-2">
<input
className="border p-2 rounded w-full"
placeholder="Enter modelId or partId"
value={query}
onChange={e => setQuery(e.target.value)}
/>
<button
className="px-4 py-2 bg-primary text-white rounded"
onClick={compare}
>
Compare
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
{result.map(item =>
item.model ? (
<ModelCard key={item.model.id} model={item.model} />
) : (
<PartCard key={item.part.id} part={item.part} />
)
)}
</div>
</div>
);
}
EOF
#############################################
# UI: Home Page Search Integration
#############################################
cat > "$BASE/page.js" << 'EOF'
"use client";
import { useState } from "react";
import SearchBar from "./components/SearchBar";
import { searchModels, searchParts } from "@/src/lib/search/search";
export default function HomePage() {
const [results, setResults] = useState([]);
async function handleSearch(query) {
const models = searchModels(query);
const parts = searchParts(query);
setResults([...models, ...parts]);
}
return (
<div className="space-y-6">
<h1 className="text-4xl font-bold">RC Compatibility Explorer</h1>
<SearchBar onSearch={handleSearch} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{results.map(item =>
item.categories ? (
<div key={item.id} className="border p-4 rounded">
<strong>{item.name}</strong> — Model
</div>
) : (
<div key={item.id} className="border p-4 rounded">
<strong>{item.name}</strong> — Part
</div>
)
)}
</div>
</div>
);
}
EOF
echo "🎉 UI integration created successfully!"
+156
View File
@@ -0,0 +1,156 @@
#!/bin/zsh
echo "🎨 Applying UI polish across the app..."
BASE="src/app"
COMP="src/app/components"
mkdir -p "$COMP"
#############################################
# CategoryCard component
#############################################
cat > "$COMP/CategoryCard.js" << 'EOF'
export default function CategoryCard({ category }) {
return (
<div className="border rounded p-4 shadow hover:shadow-lg transition cursor-pointer bg-white">
<h3 className="font-bold text-lg capitalize">{category}</h3>
</div>
);
}
EOF
#############################################
# Patch ModelPage
#############################################
cat > "$BASE/model/[modelId]/page.js" << 'EOF'
import { getModel, getCompatibleParts } from "@/src/lib/ui/api";
import CategoryCard from "@/src/app/components/CategoryCard";
import PartCard from "@/src/app/components/PartCard";
export default async function ModelPage({ params }) {
const model = await getModel(params.modelId);
const parts = await getCompatibleParts(params.modelId);
if (!model) return <p className="text-red-500">Model not found</p>;
return (
<div className="space-y-6">
<div>
<h1 className="text-4xl font-bold">{model.name}</h1>
<p className="opacity-75">{model.brand} • {model.scale}</p>
</div>
<h2 className="text-2xl font-bold">Categories</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{model.categories.map(cat => (
<a key={cat} href={`/category/${model.id}/${cat}`}>
<CategoryCard category={cat} />
</a>
))}
</div>
<h2 className="text-2xl font-bold">Compatible Parts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Array.isArray(parts) && parts.length ? (
parts.map(item => <PartCard key={item.part.id} part={item.part} />)
) : (
<p>No compatible parts found.</p>
)}
</div>
</div>
);
}
EOF
#############################################
# Patch PartPage
#############################################
cat > "$BASE/part/[partId]/page.js" << 'EOF'
import { getPart, getCompatibleModels } from "@/src/lib/ui/api";
import ModelCard from "@/src/app/components/ModelCard";
export default async function PartDetailPage({ params }) {
const part = await getPart(params.partId);
const models = await getCompatibleModels(params.partId);
if (!part) return <p className="text-red-500">Part not found</p>;
return (
<div className="space-y-6">
<div>
<h1 className="text-4xl font-bold">{part.name}</h1>
<p className="opacity-75">Part ID: {part.id}</p>
{part.universalFit && (
<span className="inline-block mt-2 px-3 py-1 bg-green-200 text-green-800 text-sm rounded">
Universal Fit
</span>
)}
{part.upgrade && (
<span className="inline-block mt-2 px-3 py-1 bg-blue-200 text-blue-800 text-sm rounded">
Upgrade Part
</span>
)}
</div>
<h2 className="text-2xl font-bold">Compatible Models</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Array.isArray(models) && models.length ? (
models.map(item => <ModelCard key={item.model.id} model={item.model} />)
) : (
<p>No compatible models found.</p>
)}
</div>
</div>
);
}
EOF
#############################################
# Patch CategoryPage
#############################################
cat > "$BASE/category/[modelId]/[categoryName]/page.js" << 'EOF'
import { getModel, getCompatibleParts } from "@/src/lib/ui/api";
import PartCard from "@/src/app/components/PartCard";
export default async function CategoryPage({ params }) {
const model = await getModel(params.modelId);
const allParts = await getCompatibleParts(params.modelId);
if (!model) return <p className="text-red-500">Model not found</p>;
const parts = allParts.filter(
p => p.part.category === params.categoryName
);
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold capitalize">
{params.categoryName} for {model.name}
</h1>
<a href={`/model/${model.id}`} className="text-blue-600 underline">
← Back to model
</a>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{parts.length ? (
parts.map(p => (
<PartCard key={p.part.id} part={p.part} />
))
) : (
<p>No compatible parts in this category.</p>
)}
</div>
</div>
);
}
EOF
echo "🎉 UI polish applied!"
+265
View File
@@ -0,0 +1,265 @@
#!/bin/zsh
echo "🚀 Setting up RC Compatibility App (Next.js + Tailwind v4 + Sass)..."
BASE="src/app"
#############################################
# 1️⃣ CREATE DIRECTORY STRUCTURE
#############################################
mkdir -p "$BASE"
mkdir -p "$BASE/model/[modelId]"
mkdir -p "$BASE/category/[modelId]/[categoryName]"
mkdir -p "$BASE/part/[partId]"
mkdir -p "$BASE/compatibility"
mkdir -p "$BASE/garage"
mkdir -p "$BASE/components"
mkdir -p "$BASE/styles/components"
#############################################
# 2️⃣ WRITE ROOT LAYOUT + MAIN PAGES
#############################################
cat > "$BASE/layout.js" << 'EOF'
import "./styles/globals.scss";
export const metadata = {
title: "RC Compatibility Explorer",
description: "Find compatibility across RC models and parts",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
EOF
cat > "$BASE/page.js" << 'EOF'
export default function HomePage() {
return (
<div>
<h1 className="text-4xl font-bold mb-4 text-primary">RC Compatibility Explorer</h1>
<p>Search for RC models, parts, and compare compatibility.</p>
</div>
);
}
EOF
#############################################
# 3️⃣ MODEL PAGE
#############################################
cat > "$BASE/model/[modelId]/page.js" << 'EOF'
export default function ModelPage({ params }) {
const { modelId } = params;
return (
<div>
<h1 className="text-2xl font-bold mb-2 text-primary">Model: {modelId}</h1>
<p>Details and component categories for this RC model.</p>
</div>
);
}
EOF
#############################################
# 4️⃣ CATEGORY PAGE
#############################################
cat > "$BASE/category/[modelId]/[categoryName]/page.js" << 'EOF'
export default function CategoryPage({ params }) {
const { modelId, categoryName } = params;
return (
<div>
<h1 className="text-2xl font-bold text-primary">{categoryName} for {modelId}</h1>
<p>List of compatible parts for this category.</p>
</div>
);
}
EOF
#############################################
# 5️⃣ PART DETAIL PAGE
#############################################
cat > "$BASE/part/[partId]/page.js" << 'EOF'
export default function PartDetailPage({ params }) {
const { partId } = params;
return (
<div>
<h1 className="text-2xl font-bold text-primary">Part Detail: {partId}</h1>
<p>Specifications and compatibility info.</p>
</div>
);
}
EOF
#############################################
# 6️⃣ COMPATIBILITY EXPLORER
#############################################
cat > "$BASE/compatibility/page.js" << 'EOF'
export default function CompatibilityPage() {
return (
<div>
<h1 className="text-3xl font-bold text-primary mb-4">Compatibility Explorer</h1>
<p>Compare RC models and parts side-by-side.</p>
</div>
);
}
EOF
#############################################
# 7️⃣ GARAGE
#############################################
cat > "$BASE/garage/page.js" << 'EOF'
export default function GaragePage() {
return (
<div>
<h1 className="text-3xl font-bold text-primary mb-4">Your Garage</h1>
<p>Save your RC models and parts here.</p>
</div>
);
}
EOF
#############################################
# 8️⃣ COMPONENTS
#############################################
cat > "$BASE/components/SearchBar.js" << 'EOF'
export default function SearchBar() {
return (
<input
type="text"
placeholder="Search..."
className="border p-2 rounded w-full"
/>
);
}
EOF
cat > "$BASE/components/ModelCard.js" << 'EOF'
export default function ModelCard({ model }) {
return (
<div className="model-card">
<h2 className="font-bold">{model.name}</h2>
<p>{model.scale} Scale</p>
</div>
);
}
EOF
cat > "$BASE/styles/components/model-card.scss" << 'EOF'
.model-card {
@apply border rounded p-4 shadow;
background: $card-bg;
}
EOF
cat > "$BASE/components/PartCard.js" << 'EOF'
export default function PartCard({ part }) {
return (
<div className="part-card">
<h2 className="font-bold">{part.name}</h2>
<p>ID: {part.id}</p>
</div>
);
}
EOF
cat > "$BASE/styles/components/part-card.scss" << 'EOF'
.part-card {
@apply border rounded p-4 shadow;
background: $card-bg;
}
EOF
cat > "$BASE/components/NavBar.js" << 'EOF'
import Link from "next/link";
import "../styles/components/navbar.scss";
export default function NavBar() {
return (
<nav className="navbar">
<Link href="/">Home</Link>
<Link href="/compatibility">Compatibility</Link>
<Link href="/garage">Garage</Link>
</nav>
);
}
EOF
cat > "$BASE/styles/components/navbar.scss" << 'EOF'
.navbar {
@apply flex gap-4 p-4 border-b;
background: $nav-bg;
}
EOF
#############################################
# 9️⃣ INSTALL TAILWIND v4 + SASS
#############################################
echo "📦 Installing Tailwind v4 & Sass..."
npm install -D tailwindcss@latest sass
#############################################
# 🔟 GLOBAL STYLES + VARIABLES
#############################################
cat > "$BASE/styles/variables.scss" << 'EOF'
$background: #ffffff;
$text-color: #111111;
$primary: #0070f3;
$secondary: #7928ca;
$card-bg: #f8f8f8;
$nav-bg: #fafafa;
:root {
--color-primary: #0070f3;
--color-secondary: #7928ca;
}
EOF
cat > "$BASE/styles/globals.scss" << 'EOF'
@import "tailwindcss";
@import "./variables";
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: $background;
color: $text-color;
}
EOF
#############################################
# 1️⃣1️⃣ OPTIONAL: TAILWIND CONFIG (THEME COLORS)
#############################################
cat > tailwind.config.js << 'EOF'
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
colors: {
primary: "var(--color-primary)",
secondary: "var(--color-secondary)"
}
}
}
};
EOF
echo "🎉 RC Compatibility App fully set up with Next.js + Tailwind v4 + Sass!"