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