added in admin pages to add models and parts
This commit is contained in:
+107
@@ -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!"
|
||||
Executable
+73
@@ -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!"
|
||||
Executable
+39
@@ -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!"
|
||||
Executable
+42
@@ -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
@@ -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!"
|
||||
Executable
+110
@@ -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
@@ -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"
|
||||
Executable
+239
@@ -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!"
|
||||
Executable
+104
@@ -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!"
|
||||
Executable
+300
@@ -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!"
|
||||
Executable
+156
@@ -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!"
|
||||
Executable
+265
@@ -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!"
|
||||
|
||||
Reference in New Issue
Block a user