basic seach small json data set models and parts

This commit is contained in:
2025-11-24 12:51:14 +00:00
parent b064d458c4
commit ffe7ea5d75
58 changed files with 3006 additions and 81 deletions
+21
View File
@@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{"name": "Launch Chrome against localhost",
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
+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!"
+36
View File
@@ -0,0 +1,36 @@
#!/bin/zsh
echo "🔧 Fixing incorrect '@/src/...' imports..."
# Search for JS, JSX, TS, TSX files under src/ only
FILES=$(find src -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \))
for FILE in $FILES; do
echo "📄 Checking $FILE"
# Fix @/src/lib → @/lib
if grep -q '@/src/lib' "$FILE"; then
echo " ➜ Fixing '@/src/lib' → '@/lib'"
sed -i '' 's#@/src/lib#@/lib#g' "$FILE"
fi
# Fix @/src/app → @/app
if grep -q '@/src/app' "$FILE"; then
echo " ➜ Fixing '@/src/app' → '@/app'"
sed -i '' 's#@/src/app#@/app#g' "$FILE"
fi
# Fix @/src/components → @/components
if grep -q '@/src/components' "$FILE"; then
echo " ➜ Fixing '@/src/components' → '@/components'"
sed -i '' 's#@/src/components#@/components#g' "$FILE"
fi
# Fix @/src/styles → @/styles
if grep -q '@/src/styles' "$FILE"; then
echo " ➜ Fixing '@/src/styles' → '@/styles'"
sed -i '' 's#@/src/styles#@/styles#g' "$FILE"
fi
done
echo "🎉 Import alias fixes complete!"
+477 -1
View File
@@ -7,16 +7,22 @@
"": { "": {
"name": "rc-compat", "name": "rc-compat",
"version": "0.1.0", "version": "0.1.0",
"license": "ISC",
"dependencies": { "dependencies": {
"fs": "^0.0.1-security",
"next": "16.0.3", "next": "16.0.3",
"path": "^0.12.7",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0" "react-dom": "19.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"autoprefixer": "^10.4.22",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.0.3", "eslint-config-next": "16.0.3",
"tailwindcss": "^4" "postcss": "^8.5.6",
"sass": "^1.94.2",
"tailwindcss": "^4.1.17"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@@ -1143,6 +1149,315 @@
"node": ">=12.4.0" "node": ">=12.4.0"
} }
}, },
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher/node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"optional": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2210,6 +2525,43 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/autoprefixer": {
"version": "10.4.22",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
"integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"browserslist": "^4.27.0",
"caniuse-lite": "^1.0.30001754",
"fraction.js": "^5.3.4",
"normalize-range": "^0.1.2",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2404,6 +2756,21 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/client-only": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3349,6 +3716,24 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w=="
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3646,6 +4031,12 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"devOptional": true
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -3671,6 +4062,11 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4700,12 +5096,28 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"optional": true
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.27", "version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true "dev": true
}, },
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4897,6 +5309,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
"dependencies": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4975,6 +5396,12 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -4984,6 +5411,14 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -5049,6 +5484,19 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true "dev": true
}, },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"devOptional": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -5214,6 +5662,26 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/sass": {
"version": "1.94.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz",
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
"devOptional": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -5955,6 +6423,14 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"dependencies": {
"inherits": "2.0.3"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+12 -2
View File
@@ -9,14 +9,24 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"fs": "^0.0.1-security",
"next": "16.0.3", "next": "16.0.3",
"path": "^0.12.7",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0" "react-dom": "19.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"autoprefixer": "^10.4.22",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.0.3", "eslint-config-next": "16.0.3",
"tailwindcss": "^4" "postcss": "^8.5.6",
} "sass": "^1.94.2",
"tailwindcss": "^4.1.17"
},
"description": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).",
"main": "tailwind.config.js",
"keywords": [],
"author": "",
"license": "ISC"
} }
+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!"
+12
View File
@@ -0,0 +1,12 @@
import fs from "fs";
import path from "path";
const PARTS_PATH = path.join(process.cwd(), "src/lib/data/parts.js");
let content = fs.readFileSync(PARTS_PATH, "utf8");
content = content.replace(/fitsModels/g, "compatibleModels");
fs.writeFileSync(PARTS_PATH, content);
console.log("✔ Migration complete: fitsModels → compatibleModels");
+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!"
+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!"
Executable
+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!"
@@ -0,0 +1,16 @@
import { getCompatibleParts } from "@/lib/compatibility/engine";
export async function GET(request, context) {
const { id } = await context.params; // ⬅ MUST AWAIT PARAMS
const results = getCompatibleParts(id);
if (!results.length) {
return Response.json(
{ error: "No compatible parts or model not found" },
{ status: 404 }
);
}
return Response.json(results);
}
@@ -0,0 +1,16 @@
import { getCrossCompatibleModels } from "@/lib/compatibility/engine";
export async function GET(request, context) {
const { id } = await context.params; // ⬅ MUST AWAIT PARAMS
const results = getCrossCompatibleModels(id);
if (!results.length) {
return Response.json(
{ error: "No compatible models or part not found" },
{ status: 404 }
);
}
return Response.json(results);
}
+11
View File
@@ -0,0 +1,11 @@
import { MODELS } from "@/lib/data/models";
export async function GET(request, context) {
const { id } = await context.params; // ⬅ MUST AWAIT PARAMS
const model = MODELS.find((m) => m.id === id);
if (!model) {
return Response.json({ error: "Model not found" }, { status: 404 });
}
return Response.json(model);
}
+5
View File
@@ -0,0 +1,5 @@
import { MODELS } from "@/lib/data/models";
export async function GET() {
return Response.json(MODELS);
}
+11
View File
@@ -0,0 +1,11 @@
import { PARTS } from "@/lib/data/parts";
export async function GET(request, context) {
const { id } = await context.params; // ⬅ MUST AWAIT PARAMS
const part = PARTS.find((p) => p.id === id);
if (!part) {
return Response.json({ error: "Part not found" }, { status: 404 });
}
return Response.json(part);
}
+5
View File
@@ -0,0 +1,5 @@
import { PARTS } from "@/lib/data/parts";
export async function GET() {
return Response.json(PARTS);
}
@@ -0,0 +1,53 @@
"use client";
import { useEffect, useState } from "react";
import { getPartsByModel } from "@/lib/ui/api";
import SkeletonCard from "@/app/components/SkeletonCard";
import PartCard from "@/app/components/PartCard";
export default function PartGrid({ modelId, categoryName }) {
const [parts, setParts] = useState(null);
useEffect(() => {
async function load() {
const allParts = await getPartsByModel(modelId);
if (!Array.isArray(allParts)) {
console.error("❌ getPartsByModel() returned:", allParts);
setParts([]); // Prevents filter crash
return;
}
const filtered = allParts.filter(
(p) => p.part.category.toLowerCase() === categoryName.toLowerCase()
);
setParts(filtered);
}
load();
}, [modelId, categoryName]);
// Skeleton loading
if (parts === null) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
);
}
// No parts found
if (parts.length === 0) {
return <p>No parts found in this category.</p>;
}
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{parts.map((part) => (
<PartCard key={part.part.id} part={part} />
))}
</div>
);
}
@@ -0,0 +1,44 @@
import Link from "next/link";
import PartGrid from "./PartGrid";
export default async function CategoryPage({ params }) {
const { modelId, categoryName } = await params;
return (
<div className="space-y-10">
{/* Breadcrumb */}
<nav className="text-sm text-gray-500 flex gap-2 items-center">
<Link href="/" className="hover:underline text-blue-600">
Home
</Link>
<span></span>
<Link
href={`/model/${modelId}`}
className="hover:underline text-blue-600 capitalize"
>
{modelId.replace(/-/g, " ")}
</Link>
<span></span>
<span className="capitalize text-gray-700 font-semibold">
{categoryName}
</span>
</nav>
{/* Title */}
<div>
<h1 className="text-4xl font-bold mb-2 capitalize">
{categoryName} Parts
</h1>
<p className="text-gray-600">
Explore compatible parts for{" "}
<span className="font-semibold">{modelId.replace(/-/g, " ")}</span>
</p>
</div>
{/* Parts Grid with Skeleton Loading */}
<PartGrid modelId={modelId} categoryName={categoryName} />
</div>
);
}
+51
View File
@@ -0,0 +1,51 @@
"use client";
import { useState } from "react";
import { fetchJSON } from "@/lib/ui/fetch";
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
type="button"
className="px-4 py-2 text-white rounded"
style={{ backgroundColor: "var(--color-primary, #0070f3)" }}
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>
);
}
+7
View File
@@ -0,0 +1,7 @@
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>
);
}
@@ -0,0 +1,7 @@
export default function CompatibilityStatus({ status }) {
return (
<p className="font-bold">
Compatibility: {status ? "Compatible" : "Not Compatible"}
</p>
);
}
+8
View File
@@ -0,0 +1,8 @@
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>
);
}
+9
View File
@@ -0,0 +1,9 @@
export default function ModelSummary({ model }) {
return (
<div>
<h2 className="font-bold text-lg">Summary</h2>
<p>Name: {model.name}</p>
<p>Scale: {model.scale}</p>
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
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>
);
}
+41
View File
@@ -0,0 +1,41 @@
import Link from "next/link";
export default function PartCard({ part }) {
part = part.hasOwnProperty("result") ? part.part : part;
const placeholderImg = `https://placehold.co/40x25?text=${encodeURIComponent(
part.name
)}`;
return (
<Link
href={`/part/${part.id}`}
className="block border rounded-xl p-5 shadow-sm hover:shadow-md hover:-translate-y-1 transition bg-white"
>
<div className="flex flex-col h-full justify-between">
{/* Image */}
<img
src={placeholderImg}
alt={part.name}
className="rounded mb-4 w-full object-cover"
/>
{/* Title */}
<div>
<h3 className="text-lg font-bold mb-1">{part.name}</h3>
<p className="text-sm text-gray-500 mb-3">{part.id}</p>
{/* Small Category Tag */}
<span className="inline-block px-3 py-1 text-xs rounded-full bg-blue-100 text-blue-700 capitalize">
{part.category}
</span>
</div>
{/* Model Count */}
<p className="mt-4 text-sm text-gray-500">
Compatible with <strong>{part.fitsModels?.length || 0}</strong>{" "}
model(s)
</p>
</div>
</Link>
);
}
+37
View File
@@ -0,0 +1,37 @@
"use client";
import { useState } from "react";
export default function SearchBar({ onSearch }) {
const [query, setQuery] = useState("");
function handleKeyDown(e) {
if (e.key === "Enter") {
e.preventDefault(); // Prevents form submission / reloads
onSearch(query.trim()); // Fire search
}
}
function handleSearchClick() {
onSearch(query.trim());
}
return (
<div className="flex gap-3 w-full max-w-xl">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search for a model or part…"
className="flex-1 border rounded-xl px-4 py-2 shadow-sm focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={handleSearchClick}
className="px-5 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition"
>
Search
</button>
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
import Link from "next/link";
export default function SearchResultCard({ item }) {
const isModel = !!item.categories; // Models have .categories
const href = isModel ? `/model/${item.id}` : `/part/${item.id}`;
const placeholderImg = `https://placehold.co/400x250?text=${encodeURIComponent(
item.name
)}`;
return (
<Link
href={href}
className="block border rounded-xl p-5 shadow-sm hover:shadow-md hover:-translate-y-1 transition bg-white"
>
{/* Image */}
<img
src={placeholderImg}
alt={item.name}
className="rounded mb-4 w-full object-cover"
/>
{/* Name */}
<h3 className="text-lg font-bold mb-1">{item.name}</h3>
{/* Type Tag */}
<span
className={`inline-block px-3 py-1 text-xs rounded-full capitalize ${
isModel
? "bg-purple-100 text-purple-700"
: "bg-blue-100 text-blue-700"
}`}
>
{isModel ? "Model" : "Part"}
</span>
{/* Extra meta */}
<p className="mt-3 text-sm text-gray-500">
ID: <span className="font-mono">{item.id}</span>
</p>
</Link>
);
}
+9
View File
@@ -0,0 +1,9 @@
export default function SearchSkeletonCard() {
return (
<div className="animate-pulse border rounded-xl p-5 bg-gray-100/50 shadow-sm">
<div className="h-32 bg-gray-300 rounded mb-4"></div>
<div className="h-4 bg-gray-300 rounded w-2/3 mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-1/3"></div>
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
export default function SkeletonCard() {
return (
<div className="animate-pulse border rounded-xl p-5 bg-gray-100/50 shadow-sm">
<div className="h-32 bg-gray-300 rounded mb-4"></div>
<div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-1/2 mb-4"></div>
<div className="h-6 bg-gray-300 rounded w-1/3"></div>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
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>
);
}
+8 -19
View File
@@ -1,28 +1,17 @@
import { Geist, Geist_Mono } from "next/font/google"; import "./styles/globals.scss";
import "./globals.css"; import NavBar from "./components/NavBar";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata = { export const metadata = {
title: "Create Next App", title: "RC Compatibility Explorer",
description: "Generated by create next app", description: "Find compatibility across RC models and parts",
}; };
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<body <body suppressHydrationWarning>
className={`${geistSans.variable} ${geistMono.variable} antialiased`} <NavBar />
> <main className="p-4">{children}</main>
{children}
</body> </body>
</html> </html>
); );
+41
View File
@@ -0,0 +1,41 @@
import { getModel, getCompatibleParts } from "@/lib/ui/api";
import CategoryCard from "@/app/components/CategoryCard";
import PartCard from "@/app/components/PartCard";
export default async function ModelPage({ params }) {
const { modelId } = await params;
const model = await getModel(modelId);
const parts = await getCompatibleParts(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>
);
}
+86
View File
@@ -0,0 +1,86 @@
"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>
);
}
+102 -59
View File
@@ -1,65 +1,108 @@
import Image from "next/image"; // "use client";
// import { useState } from "react";
// import SearchBar from "./components/SearchBar";
// import { searchModels, searchParts } from "../lib/search/search";
// export default function HomePage() {
// const [results, setResults] = useState([]);
// 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) => {
// const isModel = !!item.categories;
// const link = isModel ? `/model/${item.id}` : `/part/${item.id}`;
// return (
// <a
// key={item.id}
// href={link}
// className="block border p-4 rounded hover:bg-gray-50"
// >
// <strong>{item.name}</strong>
// <div className="opacity-60">{isModel ? "Model" : "Part"}</div>
// </a>
// );
// })}
// </div>
// </div>
// );
// }
"use client";
import { useState } from "react";
import SearchBar from "./components/SearchBar";
import SearchResultCard from "./components/SearchResultCard";
import SearchSkeletonCard from "./components/SearchSkeletonCard";
import { searchModels, searchParts } from "@/lib/search/search";
export default function HomePage() {
const [results, setResults] = useState(null); // null = loading phase
const [loading, setLoading] = useState(false);
async function handleSearch(query) {
if (!query) {
setResults([]);
return;
}
setLoading(true);
setResults(null);
// Simulate real network delay for proper skeleton visuals
setTimeout(() => {
const models = searchModels(query);
const parts = searchParts(query);
setResults([...models, ...parts]);
setLoading(false);
}, 300);
}
export default function Home() {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <div className="space-y-10">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> {/* Title */}
<Image <h1 className="text-4xl font-bold">RC Compatibility Explorer</h1>
className="dark:invert"
src="/next.svg" {/* Search Field */}
alt="Next.js logo" <SearchBar onSearch={handleSearch} />
width={100}
height={20} {/* Skeleton Loader */}
priority {loading && (
/> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left"> {Array.from({ length: 6 }).map((_, i) => (
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> <SearchSkeletonCard key={i} />
To get started, edit the page.js file. ))}
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div> </div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row"> )}
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]" {/* Results */}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" {!loading && results?.length > 0 && (
target="_blank" <>
rel="noopener noreferrer" <h2 className="text-2xl font-semibold">Search Results</h2>
> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<Image {results.map((item) => (
className="dark:invert" <SearchResultCard key={item.id} item={item} />
src="/vercel.svg" ))}
alt="Vercel logomark" </div>
width={16} </>
height={16} )}
/>
Deploy Now {/* No Results */}
</a> {!loading && results?.length === 0 && (
<a <p>No matching models or parts found.</p>
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" )}
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div> </div>
); );
} }
+44
View File
@@ -0,0 +1,44 @@
import { getPart, getCompatibleModels } from "@/lib/ui/api";
export default async function PartDetailPage({ params }) {
const { partId } = await params;
const part = await getPart(partId);
const models = await getCompatibleModels(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) => (
<div key={item.model.id}>
<p className="font-bold">{item.model.name}</p>
</div>
))
) : (
<p>No compatible models found.</p>
)}
</div>
</div>
);
}
@@ -0,0 +1,6 @@
@use "../variables" as *;
.model-card {
@apply border rounded p-4 shadow;
background: $card-bg;
}
+7
View File
@@ -0,0 +1,7 @@
@use "../variables" as *;
@reference "tailwindcss";
.navbar {
@apply flex gap-4 p-4 border-b;
background: $nav-bg;
}
+6
View File
@@ -0,0 +1,6 @@
@use "../variables" as *;
.part-card {
@apply border rounded p-4 shadow;
background: $card-bg;
}
+7
View File
@@ -0,0 +1,7 @@
@import "tailwindcss";
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
+11
View File
@@ -0,0 +1,11 @@
@use "./variables" as *;
@use "tailwindcss";
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: $background;
color: $text-color;
}
+13
View File
@@ -0,0 +1,13 @@
$background: #ffffff;
$text-color: #111111;
$primary: #0070f3;
$secondary: #7928ca;
$card-bg: #f8f8f8;
$nav-bg: #fafafa;
:root {
--color-primary: #0070f3;
--color-secondary: #7928ca;
}
+75
View File
@@ -0,0 +1,75 @@
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
);
}
+11
View File
@@ -0,0 +1,11 @@
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);
}
+7
View File
@@ -0,0 +1,7 @@
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;
}
+11
View File
@@ -0,0 +1,11 @@
export const CATEGORIES = [
"drivetrain",
"suspension",
"chassis",
"shocks",
"electronics",
"steering",
"wheels",
"tires",
"body"
];
+29
View File
@@ -0,0 +1,29 @@
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,
}
];
+29
View File
@@ -0,0 +1,29 @@
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,
},
];
+21
View File
@@ -0,0 +1,21 @@
export function validateModel(model) {
if (!model.id) throw new Error("Model missing id");
if (!model.name) console.warn(`Model ${model.id} missing name`);
if (!Array.isArray(model.categories))
throw new Error(`Model ${model.id} categories must be an array`);
}
export function validatePart(part) {
if (!part.id) throw new Error("Part missing id");
if (!part.name) throw new Error(`Part ${part.id} missing name`);
if (typeof part.category !== "string")
throw new Error(`Part ${part.id} must have a category`);
if (
!Array.isArray(part.compatibleModels) &&
!Array.isArray(part.fitsModels)
) {
throw new Error(
`Part ${part.id} must include 'compatibleModels' or 'fitsModels'`
);
}
}
+18
View File
@@ -0,0 +1,18 @@
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)
);
}
+27
View File
@@ -0,0 +1,27 @@
import { fetchJSON, BASE_URL } from "./fetch";
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}`);
}
export async function getPartsByModel(modelId) {
return fetchJSON(`/api/compatibility/model/${modelId}`);
}
export async function getPartsByCategory(modelId, categoryName) {
return fetchJSON(
`/api/compatibility/model/${modelId}?category=${categoryName}`
);
}
+18
View File
@@ -0,0 +1,18 @@
const port = process.env.PORT || 3000;
// BASE_URL resolved for server & browser
export const BASE_URL =
typeof window === "undefined"
? process.env.API_ROOT || `http://localhost:${port}`
: "";
// Universal JSON fetcher
export async function fetchJSON(path) {
const url = BASE_URL + path;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) return null;
return res.json();
}
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
colors: {
primary: "var(--color-primary)",
secondary: "var(--color-secondary)"
}
}
}
};