From ffe7ea5d758d01dd608d6382ca6539d179b1630c Mon Sep 17 00:00:00 2001 From: robbond Date: Mon, 24 Nov 2025 12:51:14 +0000 Subject: [PATCH] basic seach small json data set models and parts --- .vscode/launch.json | 21 + dynamic-route-healthcheck.sh | 107 ++++ fix-api-dynamic-routes.sh | 73 +++ fix-import-aliases.sh | 36 ++ package-lock.json | 478 +++++++++++++++++- package.json | 14 +- patch-dynamic-routes.sh | 39 ++ patch-scss-imports.sh | 42 ++ patch-tailwind-reference.sh | 22 + scripts/migrateParts.js | 12 + setup-api-layer.sh | 110 ++++ setup-data-layer.sh | 239 +++++++++ setup-models-browser.sh | 104 ++++ setup-ui-integration.sh | 300 +++++++++++ setup-ui-polish.sh | 156 ++++++ setuprc.sh | 265 ++++++++++ src/app/api/compatibility/model/[id]/route.js | 16 + src/app/api/compatibility/part/[id]/route.js | 16 + src/app/api/models/[id]/route.js | 11 + src/app/api/models/route.js | 5 + src/app/api/parts/[id]/route.js | 11 + src/app/api/parts/route.js | 5 + .../[modelId]/[categoryName]/PartGrid.js | 53 ++ .../category/[modelId]/[categoryName]/page.js | 44 ++ src/app/compatibility/page.js | 51 ++ src/app/components/CategoryCard.js | 7 + src/app/components/CompatibilityStatus.js | 7 + src/app/components/ModelCard.js | 8 + src/app/components/ModelSummary.js | 9 + src/app/components/NavBar.js | 12 + src/app/components/PartCard.js | 41 ++ src/app/components/SearchBar.js | 37 ++ src/app/components/SearchResultCard.js | 43 ++ src/app/components/SearchSkeletonCard.js | 9 + src/app/components/SkeletonCard.js | 10 + src/app/garage/page.js | 8 + src/app/layout.js | 27 +- src/app/model/[modelId]/page.js | 41 ++ src/app/models/page.js | 86 ++++ src/app/page.js | 161 +++--- src/app/part/[partId]/page.js | 44 ++ src/app/styles/components/model-card.scss | 6 + src/app/styles/components/navbar.scss | 7 + src/app/styles/components/part-card.scss | 6 + src/app/styles/globals.css | 7 + src/app/styles/globals.scss | 11 + src/app/styles/variables.scss | 13 + src/lib/compatibility/engine.js | 75 +++ src/lib/compatibility/scoring.js | 11 + src/lib/compatibility/utils.js | 7 + src/lib/data/categories.js | 11 + src/lib/data/models.js | 29 ++ src/lib/data/parts.js | 29 ++ src/lib/data/schema.js | 21 + src/lib/search/search.js | 18 + src/lib/ui/api.js | 27 + src/lib/ui/fetch.js | 18 + tailwind.config.js | 11 + 58 files changed, 3006 insertions(+), 81 deletions(-) create mode 100644 .vscode/launch.json create mode 100755 dynamic-route-healthcheck.sh create mode 100755 fix-api-dynamic-routes.sh create mode 100755 fix-import-aliases.sh create mode 100755 patch-dynamic-routes.sh create mode 100755 patch-scss-imports.sh create mode 100755 patch-tailwind-reference.sh create mode 100644 scripts/migrateParts.js create mode 100755 setup-api-layer.sh create mode 100755 setup-data-layer.sh create mode 100755 setup-models-browser.sh create mode 100755 setup-ui-integration.sh create mode 100755 setup-ui-polish.sh create mode 100755 setuprc.sh create mode 100644 src/app/api/compatibility/model/[id]/route.js create mode 100644 src/app/api/compatibility/part/[id]/route.js create mode 100644 src/app/api/models/[id]/route.js create mode 100644 src/app/api/models/route.js create mode 100644 src/app/api/parts/[id]/route.js create mode 100644 src/app/api/parts/route.js create mode 100644 src/app/category/[modelId]/[categoryName]/PartGrid.js create mode 100644 src/app/category/[modelId]/[categoryName]/page.js create mode 100644 src/app/compatibility/page.js create mode 100644 src/app/components/CategoryCard.js create mode 100644 src/app/components/CompatibilityStatus.js create mode 100644 src/app/components/ModelCard.js create mode 100644 src/app/components/ModelSummary.js create mode 100644 src/app/components/NavBar.js create mode 100644 src/app/components/PartCard.js create mode 100644 src/app/components/SearchBar.js create mode 100644 src/app/components/SearchResultCard.js create mode 100644 src/app/components/SearchSkeletonCard.js create mode 100644 src/app/components/SkeletonCard.js create mode 100644 src/app/garage/page.js create mode 100644 src/app/model/[modelId]/page.js create mode 100644 src/app/models/page.js create mode 100644 src/app/part/[partId]/page.js create mode 100644 src/app/styles/components/model-card.scss create mode 100644 src/app/styles/components/navbar.scss create mode 100644 src/app/styles/components/part-card.scss create mode 100644 src/app/styles/globals.css create mode 100644 src/app/styles/globals.scss create mode 100644 src/app/styles/variables.scss create mode 100644 src/lib/compatibility/engine.js create mode 100644 src/lib/compatibility/scoring.js create mode 100644 src/lib/compatibility/utils.js create mode 100644 src/lib/data/categories.js create mode 100644 src/lib/data/models.js create mode 100644 src/lib/data/parts.js create mode 100644 src/lib/data/schema.js create mode 100644 src/lib/search/search.js create mode 100644 src/lib/ui/api.js create mode 100644 src/lib/ui/fetch.js create mode 100644 tailwind.config.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8f9df64 --- /dev/null +++ b/.vscode/launch.json @@ -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}" + } + + ] +} \ No newline at end of file diff --git a/dynamic-route-healthcheck.sh b/dynamic-route-healthcheck.sh new file mode 100755 index 0000000..ee68765 --- /dev/null +++ b/dynamic-route-healthcheck.sh @@ -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!" diff --git a/fix-api-dynamic-routes.sh b/fix-api-dynamic-routes.sh new file mode 100755 index 0000000..c71f8c6 --- /dev/null +++ b/fix-api-dynamic-routes.sh @@ -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!" diff --git a/fix-import-aliases.sh b/fix-import-aliases.sh new file mode 100755 index 0000000..d1a8b02 --- /dev/null +++ b/fix-import-aliases.sh @@ -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!" diff --git a/package-lock.json b/package-lock.json index 7c3d903..ba0b8ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,16 +7,22 @@ "": { "name": "rc-compat", "version": "0.1.0", + "license": "ISC", "dependencies": { + "fs": "^0.0.1-security", "next": "16.0.3", + "path": "^0.12.7", "react": "19.2.0", "react-dom": "19.2.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "autoprefixer": "^10.4.22", "eslint": "^9", "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": { @@ -1143,6 +1149,315 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2210,6 +2525,43 @@ "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": { "version": "1.0.7", "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" } }, + "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3349,6 +3716,24 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3646,6 +4031,12 @@ "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": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3671,6 +4062,11 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4700,12 +5096,28 @@ "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": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4897,6 +5309,15 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4975,6 +5396,12 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4984,6 +5411,14 @@ "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": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5049,6 +5484,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "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": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5214,6 +5662,26 @@ "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": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5955,6 +6423,14 @@ "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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index ad27f28..a65c4ac 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,24 @@ "lint": "eslint" }, "dependencies": { + "fs": "^0.0.1-security", "next": "16.0.3", + "path": "^0.12.7", "react": "19.2.0", "react-dom": "19.2.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "autoprefixer": "^10.4.22", "eslint": "^9", "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" } diff --git a/patch-dynamic-routes.sh b/patch-dynamic-routes.sh new file mode 100755 index 0000000..9b63c46 --- /dev/null +++ b/patch-dynamic-routes.sh @@ -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!" diff --git a/patch-scss-imports.sh b/patch-scss-imports.sh new file mode 100755 index 0000000..b4f6ce5 --- /dev/null +++ b/patch-scss-imports.sh @@ -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!" diff --git a/patch-tailwind-reference.sh b/patch-tailwind-reference.sh new file mode 100755 index 0000000..4148a8e --- /dev/null +++ b/patch-tailwind-reference.sh @@ -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!" diff --git a/scripts/migrateParts.js b/scripts/migrateParts.js new file mode 100644 index 0000000..74f0947 --- /dev/null +++ b/scripts/migrateParts.js @@ -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"); diff --git a/setup-api-layer.sh b/setup-api-layer.sh new file mode 100755 index 0000000..8873d3b --- /dev/null +++ b/setup-api-layer.sh @@ -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!" diff --git a/setup-data-layer.sh b/setup-data-layer.sh new file mode 100755 index 0000000..6a862b0 --- /dev/null +++ b/setup-data-layer.sh @@ -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!" diff --git a/setup-models-browser.sh b/setup-models-browser.sh new file mode 100755 index 0000000..b452894 --- /dev/null +++ b/setup-models-browser.sh @@ -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 ( +
+

All Models

+ + {/* Filters */} +
+
+ + +
+ +
+ + +
+ + +
+ + {/* Model Grid */} +
+ {filtered.map(model => ( + + + + ))} +
+
+ ); +} +EOF + +echo "πŸŽ‰ Models Browser Page created!" diff --git a/setup-ui-integration.sh b/setup-ui-integration.sh new file mode 100755 index 0000000..6742ef7 --- /dev/null +++ b/setup-ui-integration.sh @@ -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 ( +
+

{model.name}

+

{model.brand} β€’ {model.scale}

+
+ ); +} +EOF + +# PartCard.js +cat > "$COMP/PartCard.js" << 'EOF' +export default function PartCard({ part }) { + return ( +
+

{part.name}

+

ID: {part.id}

+
+ ); +} +EOF + +# SearchBar.js +cat > "$COMP/SearchBar.js" << 'EOF' +"use client"; + +import { useState } from "react"; + +export default function SearchBar({ onSearch }) { + const [text, setText] = useState(""); + + return ( +
+ setText(e.target.value)} + /> + +
+ ); +} +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

Model not found

; + + return ( +
+

{model.name}

+

{model.brand} β€’ {model.scale}

+ +

Compatible Parts

+
+ {Array.isArray(parts) && parts.length > 0 ? ( + parts.map(item => ) + ) : ( +

No compatible parts.

+ )} +
+
+ ); +} +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

Part not found

; + + return ( +
+

{part.name}

+

ID: {part.id}

+ +

Compatible Models

+
+ {Array.isArray(models) && models.length > 0 ? ( + models.map(item => ( + + )) + ) : ( +

No compatible models.

+ )} +
+
+ ); +} +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

Model not found

; + + const parts = allParts + ? allParts.filter(p => p.part.category === params.categoryName) + : []; + + return ( +
+

+ {params.categoryName} for {model.name} +

+ +
+ {parts.length ? ( + parts.map(p => ( + + )) + ) : ( +

No compatible parts in this category.

+ )} +
+
+ ); +} +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 ( +
+

Compatibility Explorer

+ +
+ setQuery(e.target.value)} + /> + +
+ +
+ {result.map(item => + item.model ? ( + + ) : ( + + ) + )} +
+
+ ); +} +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 ( +
+

RC Compatibility Explorer

+ + + +
+ {results.map(item => + item.categories ? ( +
+ {item.name} β€” Model +
+ ) : ( +
+ {item.name} β€” Part +
+ ) + )} +
+
+ ); +} +EOF + +echo "πŸŽ‰ UI integration created successfully!" diff --git a/setup-ui-polish.sh b/setup-ui-polish.sh new file mode 100755 index 0000000..cce274a --- /dev/null +++ b/setup-ui-polish.sh @@ -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 ( +
+

{category}

+
+ ); +} +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

Model not found

; + + return ( +
+
+

{model.name}

+

{model.brand} β€’ {model.scale}

+
+ +

Categories

+
+ {model.categories.map(cat => ( + + + + ))} +
+ +

Compatible Parts

+
+ {Array.isArray(parts) && parts.length ? ( + parts.map(item => ) + ) : ( +

No compatible parts found.

+ )} +
+
+ ); +} +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

Part not found

; + + return ( +
+
+

{part.name}

+

Part ID: {part.id}

+ + {part.universalFit && ( + + Universal Fit + + )} + + {part.upgrade && ( + + Upgrade Part + + )} +
+ +

Compatible Models

+
+ {Array.isArray(models) && models.length ? ( + models.map(item => ) + ) : ( +

No compatible models found.

+ )} +
+
+ ); +} +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

Model not found

; + + const parts = allParts.filter( + p => p.part.category === params.categoryName + ); + + return ( +
+

+ {params.categoryName} for {model.name} +

+ + + ← Back to model + + +
+ {parts.length ? ( + parts.map(p => ( + + )) + ) : ( +

No compatible parts in this category.

+ )} +
+
+ ); +} +EOF + +echo "πŸŽ‰ UI polish applied!" diff --git a/setuprc.sh b/setuprc.sh new file mode 100755 index 0000000..50dfbf9 --- /dev/null +++ b/setuprc.sh @@ -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 ( + + + {children} + + + ); +} +EOF + +cat > "$BASE/page.js" << 'EOF' +export default function HomePage() { + return ( +
+

RC Compatibility Explorer

+

Search for RC models, parts, and compare compatibility.

+
+ ); +} +EOF + +############################################# +# 3️⃣ MODEL PAGE +############################################# + +cat > "$BASE/model/[modelId]/page.js" << 'EOF' +export default function ModelPage({ params }) { + const { modelId } = params; + return ( +
+

Model: {modelId}

+

Details and component categories for this RC model.

+
+ ); +} +EOF + +############################################# +# 4️⃣ CATEGORY PAGE +############################################# + +cat > "$BASE/category/[modelId]/[categoryName]/page.js" << 'EOF' +export default function CategoryPage({ params }) { + const { modelId, categoryName } = params; + return ( +
+

{categoryName} for {modelId}

+

List of compatible parts for this category.

+
+ ); +} +EOF + +############################################# +# 5️⃣ PART DETAIL PAGE +############################################# + +cat > "$BASE/part/[partId]/page.js" << 'EOF' +export default function PartDetailPage({ params }) { + const { partId } = params; + return ( +
+

Part Detail: {partId}

+

Specifications and compatibility info.

+
+ ); +} +EOF + +############################################# +# 6️⃣ COMPATIBILITY EXPLORER +############################################# + +cat > "$BASE/compatibility/page.js" << 'EOF' +export default function CompatibilityPage() { + return ( +
+

Compatibility Explorer

+

Compare RC models and parts side-by-side.

+
+ ); +} +EOF + +############################################# +# 7️⃣ GARAGE +############################################# + +cat > "$BASE/garage/page.js" << 'EOF' +export default function GaragePage() { + return ( +
+

Your Garage

+

Save your RC models and parts here.

+
+ ); +} +EOF + +############################################# +# 8️⃣ COMPONENTS +############################################# + +cat > "$BASE/components/SearchBar.js" << 'EOF' +export default function SearchBar() { + return ( + + ); +} +EOF + +cat > "$BASE/components/ModelCard.js" << 'EOF' +export default function ModelCard({ model }) { + return ( +
+

{model.name}

+

{model.scale} Scale

+
+ ); +} +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 ( +
+

{part.name}

+

ID: {part.id}

+
+ ); +} +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 ( + + ); +} +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!" + diff --git a/src/app/api/compatibility/model/[id]/route.js b/src/app/api/compatibility/model/[id]/route.js new file mode 100644 index 0000000..3023a58 --- /dev/null +++ b/src/app/api/compatibility/model/[id]/route.js @@ -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); +} diff --git a/src/app/api/compatibility/part/[id]/route.js b/src/app/api/compatibility/part/[id]/route.js new file mode 100644 index 0000000..d6e0e2a --- /dev/null +++ b/src/app/api/compatibility/part/[id]/route.js @@ -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); +} diff --git a/src/app/api/models/[id]/route.js b/src/app/api/models/[id]/route.js new file mode 100644 index 0000000..d7297b7 --- /dev/null +++ b/src/app/api/models/[id]/route.js @@ -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); +} diff --git a/src/app/api/models/route.js b/src/app/api/models/route.js new file mode 100644 index 0000000..f7c36ba --- /dev/null +++ b/src/app/api/models/route.js @@ -0,0 +1,5 @@ +import { MODELS } from "@/lib/data/models"; + +export async function GET() { + return Response.json(MODELS); +} diff --git a/src/app/api/parts/[id]/route.js b/src/app/api/parts/[id]/route.js new file mode 100644 index 0000000..787b9f0 --- /dev/null +++ b/src/app/api/parts/[id]/route.js @@ -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); +} diff --git a/src/app/api/parts/route.js b/src/app/api/parts/route.js new file mode 100644 index 0000000..8468222 --- /dev/null +++ b/src/app/api/parts/route.js @@ -0,0 +1,5 @@ +import { PARTS } from "@/lib/data/parts"; + +export async function GET() { + return Response.json(PARTS); +} diff --git a/src/app/category/[modelId]/[categoryName]/PartGrid.js b/src/app/category/[modelId]/[categoryName]/PartGrid.js new file mode 100644 index 0000000..06d4e74 --- /dev/null +++ b/src/app/category/[modelId]/[categoryName]/PartGrid.js @@ -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 ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ); + } + + // No parts found + if (parts.length === 0) { + return

No parts found in this category.

; + } + + return ( +
+ {parts.map((part) => ( + + ))} +
+ ); +} diff --git a/src/app/category/[modelId]/[categoryName]/page.js b/src/app/category/[modelId]/[categoryName]/page.js new file mode 100644 index 0000000..c6c3ef7 --- /dev/null +++ b/src/app/category/[modelId]/[categoryName]/page.js @@ -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 ( +
+ {/* Breadcrumb */} + + + {/* Title */} +
+

+ {categoryName} Parts +

+

+ Explore compatible parts for{" "} + {modelId.replace(/-/g, " ")} +

+
+ + {/* Parts Grid with Skeleton Loading */} + +
+ ); +} diff --git a/src/app/compatibility/page.js b/src/app/compatibility/page.js new file mode 100644 index 0000000..2407482 --- /dev/null +++ b/src/app/compatibility/page.js @@ -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 ( +
+

Compatibility Explorer

+ +
+ setQuery(e.target.value)} + /> + +
+ +
+ {result.map((item) => + item.model ? ( + + ) : ( + + ) + )} +
+
+ ); +} diff --git a/src/app/components/CategoryCard.js b/src/app/components/CategoryCard.js new file mode 100644 index 0000000..d148c58 --- /dev/null +++ b/src/app/components/CategoryCard.js @@ -0,0 +1,7 @@ +export default function CategoryCard({ category }) { + return ( +
+

{category}

+
+ ); +} diff --git a/src/app/components/CompatibilityStatus.js b/src/app/components/CompatibilityStatus.js new file mode 100644 index 0000000..4bac3a9 --- /dev/null +++ b/src/app/components/CompatibilityStatus.js @@ -0,0 +1,7 @@ +export default function CompatibilityStatus({ status }) { + return ( +

+ Compatibility: {status ? "Compatible" : "Not Compatible"} +

+ ); +} diff --git a/src/app/components/ModelCard.js b/src/app/components/ModelCard.js new file mode 100644 index 0000000..2f4c41f --- /dev/null +++ b/src/app/components/ModelCard.js @@ -0,0 +1,8 @@ +export default function ModelCard({ model }) { + return ( +
+

{model.name}

+

{model.brand} β€’ {model.scale}

+
+ ); +} diff --git a/src/app/components/ModelSummary.js b/src/app/components/ModelSummary.js new file mode 100644 index 0000000..babdea7 --- /dev/null +++ b/src/app/components/ModelSummary.js @@ -0,0 +1,9 @@ +export default function ModelSummary({ model }) { + return ( +
+

Summary

+

Name: {model.name}

+

Scale: {model.scale}

+
+ ); +} diff --git a/src/app/components/NavBar.js b/src/app/components/NavBar.js new file mode 100644 index 0000000..1f22b8b --- /dev/null +++ b/src/app/components/NavBar.js @@ -0,0 +1,12 @@ +import Link from "next/link"; +import "../styles/components/navbar.scss"; + +export default function NavBar() { + return ( + + ); +} diff --git a/src/app/components/PartCard.js b/src/app/components/PartCard.js new file mode 100644 index 0000000..41f7ea1 --- /dev/null +++ b/src/app/components/PartCard.js @@ -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 ( + +
+ {/* Image */} + {part.name} + + {/* Title */} +
+

{part.name}

+

{part.id}

+ + {/* Small Category Tag */} + + {part.category} + +
+ + {/* Model Count */} +

+ Compatible with {part.fitsModels?.length || 0}{" "} + model(s) +

+
+ + ); +} diff --git a/src/app/components/SearchBar.js b/src/app/components/SearchBar.js new file mode 100644 index 0000000..fa11b55 --- /dev/null +++ b/src/app/components/SearchBar.js @@ -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 ( +
+ 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" + /> + +
+ ); +} diff --git a/src/app/components/SearchResultCard.js b/src/app/components/SearchResultCard.js new file mode 100644 index 0000000..09e7b2c --- /dev/null +++ b/src/app/components/SearchResultCard.js @@ -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 ( + + {/* Image */} + {item.name} + + {/* Name */} +

{item.name}

+ + {/* Type Tag */} + + {isModel ? "Model" : "Part"} + + + {/* Extra meta */} +

+ ID: {item.id} +

+ + ); +} diff --git a/src/app/components/SearchSkeletonCard.js b/src/app/components/SearchSkeletonCard.js new file mode 100644 index 0000000..7190a10 --- /dev/null +++ b/src/app/components/SearchSkeletonCard.js @@ -0,0 +1,9 @@ +export default function SearchSkeletonCard() { + return ( +
+
+
+
+
+ ); +} diff --git a/src/app/components/SkeletonCard.js b/src/app/components/SkeletonCard.js new file mode 100644 index 0000000..9c9ffef --- /dev/null +++ b/src/app/components/SkeletonCard.js @@ -0,0 +1,10 @@ +export default function SkeletonCard() { + return ( +
+
+
+
+
+
+ ); +} diff --git a/src/app/garage/page.js b/src/app/garage/page.js new file mode 100644 index 0000000..d6d3781 --- /dev/null +++ b/src/app/garage/page.js @@ -0,0 +1,8 @@ +export default function GaragePage() { + return ( +
+

Your Garage

+

Save your RC models and parts here.

+
+ ); +} diff --git a/src/app/layout.js b/src/app/layout.js index 7bf337d..4ca9705 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -1,28 +1,17 @@ -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +import "./styles/globals.scss"; +import NavBar from "./components/NavBar"; export const metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "RC Compatibility Explorer", + description: "Find compatibility across RC models and parts", }; export default function RootLayout({ children }) { return ( - - - {children} + + + +
{children}
); diff --git a/src/app/model/[modelId]/page.js b/src/app/model/[modelId]/page.js new file mode 100644 index 0000000..e59eade --- /dev/null +++ b/src/app/model/[modelId]/page.js @@ -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

Model not found

; + + return ( +
+
+

{model.name}

+

+ {model.brand} β€’ {model.scale} +

+
+ +

Categories

+
+ {model.categories.map((cat) => ( + + + + ))} +
+ +

Compatible Parts

+
+ {Array.isArray(parts) && parts.length ? ( + parts.map((item) => ) + ) : ( +

No compatible parts found.

+ )} +
+
+ ); +} diff --git a/src/app/models/page.js b/src/app/models/page.js new file mode 100644 index 0000000..990157d --- /dev/null +++ b/src/app/models/page.js @@ -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 ( +
+

All Models

+ + {/* Filters */} +
+
+ + +
+ +
+ + +
+ + +
+ + {/* Model Grid */} +
+ {filtered.map(model => ( + + + + ))} +
+
+ ); +} diff --git a/src/app/page.js b/src/app/page.js index 691817b..9686bb0 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -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 ( +//
+//

RC Compatibility Explorer

+ +// + +//
+// {results.map((item) => { +// const isModel = !!item.categories; +// const link = isModel ? `/model/${item.id}` : `/part/${item.id}`; + +// return ( +// +// {item.name} +//
{isModel ? "Model" : "Part"}
+//
+// ); +// })} +//
+//
+// ); +// } + +"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 ( -
-
- Next.js logo -
-

- To get started, edit the page.js file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+ {/* Title */} +

RC Compatibility Explorer

+ + {/* Search Field */} + + + {/* Skeleton Loader */} + {loading && ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))}
- -
+ )} + + {/* Results */} + {!loading && results?.length > 0 && ( + <> +

Search Results

+
+ {results.map((item) => ( + + ))} +
+ + )} + + {/* No Results */} + {!loading && results?.length === 0 && ( +

No matching models or parts found.

+ )}
); } diff --git a/src/app/part/[partId]/page.js b/src/app/part/[partId]/page.js new file mode 100644 index 0000000..5bff0c5 --- /dev/null +++ b/src/app/part/[partId]/page.js @@ -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

Part not found

; + + return ( +
+
+

{part.name}

+

Part ID: {part.id}

+ + {part.universalFit && ( + + Universal Fit + + )} + + {part.upgrade && ( + + Upgrade Part + + )} +
+ +

Compatible Models

+
+ {Array.isArray(models) && models.length ? ( + models.map((item) => ( +
+

{item.model.name}

+
+ )) + ) : ( +

No compatible models found.

+ )} +
+
+ ); +} diff --git a/src/app/styles/components/model-card.scss b/src/app/styles/components/model-card.scss new file mode 100644 index 0000000..3ec3120 --- /dev/null +++ b/src/app/styles/components/model-card.scss @@ -0,0 +1,6 @@ +@use "../variables" as *; + +.model-card { + @apply border rounded p-4 shadow; + background: $card-bg; +} \ No newline at end of file diff --git a/src/app/styles/components/navbar.scss b/src/app/styles/components/navbar.scss new file mode 100644 index 0000000..375fa12 --- /dev/null +++ b/src/app/styles/components/navbar.scss @@ -0,0 +1,7 @@ +@use "../variables" as *; +@reference "tailwindcss"; + +.navbar { + @apply flex gap-4 p-4 border-b; + background: $nav-bg; +} \ No newline at end of file diff --git a/src/app/styles/components/part-card.scss b/src/app/styles/components/part-card.scss new file mode 100644 index 0000000..271c95d --- /dev/null +++ b/src/app/styles/components/part-card.scss @@ -0,0 +1,6 @@ +@use "../variables" as *; + +.part-card { + @apply border rounded p-4 shadow; + background: $card-bg; +} \ No newline at end of file diff --git a/src/app/styles/globals.css b/src/app/styles/globals.css new file mode 100644 index 0000000..bc199ac --- /dev/null +++ b/src/app/styles/globals.css @@ -0,0 +1,7 @@ +@import "tailwindcss"; + +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} diff --git a/src/app/styles/globals.scss b/src/app/styles/globals.scss new file mode 100644 index 0000000..e0d6ecf --- /dev/null +++ b/src/app/styles/globals.scss @@ -0,0 +1,11 @@ +@use "./variables" as *; +@use "tailwindcss"; + + +body { + margin: 0; + padding: 0; + font-family: sans-serif; + background: $background; + color: $text-color; +} \ No newline at end of file diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss new file mode 100644 index 0000000..23e841c --- /dev/null +++ b/src/app/styles/variables.scss @@ -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; +} diff --git a/src/lib/compatibility/engine.js b/src/lib/compatibility/engine.js new file mode 100644 index 0000000..e079efd --- /dev/null +++ b/src/lib/compatibility/engine.js @@ -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 + ); +} diff --git a/src/lib/compatibility/scoring.js b/src/lib/compatibility/scoring.js new file mode 100644 index 0000000..7824788 --- /dev/null +++ b/src/lib/compatibility/scoring.js @@ -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); +} diff --git a/src/lib/compatibility/utils.js b/src/lib/compatibility/utils.js new file mode 100644 index 0000000..e20e77f --- /dev/null +++ b/src/lib/compatibility/utils.js @@ -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; +} diff --git a/src/lib/data/categories.js b/src/lib/data/categories.js new file mode 100644 index 0000000..622b085 --- /dev/null +++ b/src/lib/data/categories.js @@ -0,0 +1,11 @@ +export const CATEGORIES = [ + "drivetrain", + "suspension", + "chassis", + "shocks", + "electronics", + "steering", + "wheels", + "tires", + "body" +]; diff --git a/src/lib/data/models.js b/src/lib/data/models.js new file mode 100644 index 0000000..30eed97 --- /dev/null +++ b/src/lib/data/models.js @@ -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, + } +]; diff --git a/src/lib/data/parts.js b/src/lib/data/parts.js new file mode 100644 index 0000000..faf86cf --- /dev/null +++ b/src/lib/data/parts.js @@ -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, + }, +]; diff --git a/src/lib/data/schema.js b/src/lib/data/schema.js new file mode 100644 index 0000000..d3f7e73 --- /dev/null +++ b/src/lib/data/schema.js @@ -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'` + ); + } +} diff --git a/src/lib/search/search.js b/src/lib/search/search.js new file mode 100644 index 0000000..77dff70 --- /dev/null +++ b/src/lib/search/search.js @@ -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) + ); +} diff --git a/src/lib/ui/api.js b/src/lib/ui/api.js new file mode 100644 index 0000000..798a05d --- /dev/null +++ b/src/lib/ui/api.js @@ -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}` + ); +} diff --git a/src/lib/ui/fetch.js b/src/lib/ui/fetch.js new file mode 100644 index 0000000..17a309d --- /dev/null +++ b/src/lib/ui/fetch.js @@ -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(); +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..038881d --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + theme: { + extend: { + colors: { + primary: "var(--color-primary)", + secondary: "var(--color-secondary)" + } + } + } +};