Added mongodb storage and fixed search
This commit is contained in:
parent
d028c67c68
commit
9cf1a287a7
@ -13,3 +13,10 @@ The Project follows the latest coding standards for next js projects
|
|||||||
This Project will use as less modules and node packages as possible. This leads to the following rules:
|
This Project will use as less modules and node packages as possible. This leads to the following rules:
|
||||||
- No Tailwind
|
- No Tailwind
|
||||||
- No SCSS / SASS
|
- No SCSS / SASS
|
||||||
|
|
||||||
|
# Code Style
|
||||||
|
|
||||||
|
This project follows clean code rules:
|
||||||
|
- SOLID Principles
|
||||||
|
- No else statements
|
||||||
|
- readable name
|
||||||
12
Dockerfile.dev
Normal file
12
Dockerfile.dev
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# CMD will be executed when container starts
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
61
docker-compose.yml
Normal file
61
docker-compose.yml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
hostname: evwiki.test
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
environment:
|
||||||
|
- MONGODB_URI=mongodb://mongodb:27017/evwiki
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.app.entrypoints=web"
|
||||||
|
- "traefik.http.routers.app.rule=Host(`evwiki.test`)"
|
||||||
|
- "traefik.http.services.app.loadbalancer.server.port=3000"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
mongodb:
|
||||||
|
image: mongo:latest
|
||||||
|
hostname: mongodb.evwiki.test
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.mongodb.entrypoints=mongodb"
|
||||||
|
- "traefik.http.routers.mongodb.rule=Host(`mongodb.evwiki.test`)"
|
||||||
|
- "traefik.http.services.mongodb.loadbalancer.server.port=27017"
|
||||||
|
|
||||||
|
mongo-express:
|
||||||
|
image: mongo-express:latest
|
||||||
|
hostname: mongo-express.evwiki.test
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
environment:
|
||||||
|
- ME_CONFIG_MONGODB_SERVER=mongodb
|
||||||
|
- ME_CONFIG_MONGODB_PORT=27017
|
||||||
|
- ME_CONFIG_MONGODB_ENABLE_ADMIN=true
|
||||||
|
- ME_CONFIG_BASICAUTH_USERNAME=admin
|
||||||
|
- ME_CONFIG_BASICAUTH_PASSWORD=pass
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.mongo-express.entrypoints=web"
|
||||||
|
- "traefik.http.routers.mongo-express.rule=Host(`mongo-express.evwiki.test`)"
|
||||||
|
- "traefik.http.services.mongo-express.loadbalancer.server.port=8081"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
140
package-lock.json
generated
140
package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"mongodb": "^6.16.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
@ -649,6 +650,15 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sparse-bitfield": "^3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.10",
|
"version": "0.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz",
|
||||||
@ -945,6 +955,21 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/webidl-conversions": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/whatwg-url": {
|
||||||
|
"version": "11.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
||||||
|
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/webidl-conversions": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.32.1",
|
"version": "8.32.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
||||||
@ -1747,6 +1772,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bson": {
|
||||||
|
"version": "6.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz",
|
||||||
|
"integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@ -3763,6 +3797,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memory-pager": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@ -3810,6 +3850,62 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mongodb": {
|
||||||
|
"version": "6.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz",
|
||||||
|
"integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@mongodb-js/saslprep": "^1.1.9",
|
||||||
|
"bson": "^6.10.3",
|
||||||
|
"mongodb-connection-string-url": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@aws-sdk/credential-providers": "^3.188.0",
|
||||||
|
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
||||||
|
"gcp-metadata": "^5.2.0",
|
||||||
|
"kerberos": "^2.0.1",
|
||||||
|
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||||
|
"snappy": "^7.2.2",
|
||||||
|
"socks": "^2.7.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@aws-sdk/credential-providers": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@mongodb-js/zstd": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"gcp-metadata": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kerberos": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mongodb-client-encryption": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"snappy": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"socks": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mongodb-connection-string-url": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/whatwg-url": "^11.0.2",
|
||||||
|
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -4226,7 +4322,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@ -4683,6 +4778,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sparse-bitfield": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"memory-pager": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||||
@ -4941,6 +5045,18 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
@ -5147,6 +5263,28 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"mongodb": "^6.16.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { CarRepository } from '../../../../../backend/repositories/CarRepository';
|
|
||||||
|
|
||||||
const carRepository = new CarRepository();
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
const brandId = params.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const carModels = await carRepository.getCarModelsByBrandId(brandId);
|
|
||||||
return NextResponse.json({ models: carModels, revisions: [] });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching car models for brand ${brandId}:`, error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch car models' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { CarRepository } from '../../../../backend/repositories/CarRepository';
|
|
||||||
|
|
||||||
const carRepository = new CarRepository();
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const brands = await carRepository.getAllBrands();
|
|
||||||
return NextResponse.json(brands);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching brands:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch brands' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,27 +6,8 @@ const carRepository = new CarRepository();
|
|||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const searchParams = request.nextUrl.searchParams;
|
||||||
const query = searchParams.get('query') || '';
|
const query = searchParams.get('query') || '';
|
||||||
const category = searchParams.get('category') || '';
|
|
||||||
const startYear = searchParams.get('startYear') ? parseInt(searchParams.get('startYear')!) : 0;
|
|
||||||
const endYear = searchParams.get('endYear') ? parseInt(searchParams.get('endYear')!) : new Date().getFullYear();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let results;
|
let results = await carRepository.searchCarsByName(query);
|
||||||
|
|
||||||
// If category is provided, search by category
|
|
||||||
if (category) {
|
|
||||||
const models = await carRepository.getCarsByCategory(category);
|
|
||||||
results = { models, revisions: [] };
|
|
||||||
}
|
|
||||||
// If startYear and endYear are provided (and not the default values), search by year range
|
|
||||||
else if (startYear > 0 || endYear < new Date().getFullYear()) {
|
|
||||||
const models = await carRepository.getCarsByYearRange(startYear, endYear);
|
|
||||||
results = { models, revisions: [] };
|
|
||||||
}
|
|
||||||
// Otherwise search by name (using query)
|
|
||||||
else {
|
|
||||||
results = await carRepository.searchCarsByName(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(results);
|
return NextResponse.json(results);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
111
src/app/components/BrandList.tsx
Normal file
111
src/app/components/BrandList.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
brandSection: {
|
||||||
|
marginTop: '2rem',
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
color: '#2e4057',
|
||||||
|
position: 'relative' as const
|
||||||
|
},
|
||||||
|
brandSectionBar: {
|
||||||
|
content: '',
|
||||||
|
position: 'absolute' as const,
|
||||||
|
top: '-15px',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: '50px',
|
||||||
|
height: '3px',
|
||||||
|
backgroundColor: '#edae49',
|
||||||
|
borderRadius: '3px'
|
||||||
|
},
|
||||||
|
brandList: {
|
||||||
|
marginTop: '0.8rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexWrap: 'wrap' as const,
|
||||||
|
gap: '1.2rem'
|
||||||
|
},
|
||||||
|
brandLink: {
|
||||||
|
color: '#083d77',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: '500',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
borderRadius: '20px',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '1px solid rgba(8, 61, 119, 0.1)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BrandList() {
|
||||||
|
return (
|
||||||
|
<div style={styles.brandSection}>
|
||||||
|
<div style={styles.brandSectionBar}></div>
|
||||||
|
<p>Popular brands</p>
|
||||||
|
<div style={styles.brandList}>
|
||||||
|
<Link
|
||||||
|
href="/results?brand=1"
|
||||||
|
style={styles.brandLink}
|
||||||
|
onMouseOver={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '#edae49';
|
||||||
|
e.currentTarget.style.color = '#2e4057';
|
||||||
|
}}
|
||||||
|
onMouseOut={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'white';
|
||||||
|
e.currentTarget.style.color = '#083d77';
|
||||||
|
}}
|
||||||
|
>Tesla</Link>
|
||||||
|
<Link
|
||||||
|
href="/results?brand=2"
|
||||||
|
style={styles.brandLink}
|
||||||
|
onMouseOver={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '#edae49';
|
||||||
|
e.currentTarget.style.color = '#2e4057';
|
||||||
|
}}
|
||||||
|
onMouseOut={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'white';
|
||||||
|
e.currentTarget.style.color = '#083d77';
|
||||||
|
}}
|
||||||
|
>BMW</Link>
|
||||||
|
<Link
|
||||||
|
href="/results?brand=3"
|
||||||
|
style={styles.brandLink}
|
||||||
|
onMouseOver={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '#edae49';
|
||||||
|
e.currentTarget.style.color = '#2e4057';
|
||||||
|
}}
|
||||||
|
onMouseOut={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'white';
|
||||||
|
e.currentTarget.style.color = '#083d77';
|
||||||
|
}}
|
||||||
|
>Toyota</Link>
|
||||||
|
<Link
|
||||||
|
href="/results?brand=4"
|
||||||
|
style={styles.brandLink}
|
||||||
|
onMouseOver={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '#edae49';
|
||||||
|
e.currentTarget.style.color = '#2e4057';
|
||||||
|
}}
|
||||||
|
onMouseOut={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'white';
|
||||||
|
e.currentTarget.style.color = '#083d77';
|
||||||
|
}}
|
||||||
|
>Audi</Link>
|
||||||
|
<Link
|
||||||
|
href="/results?brand=5"
|
||||||
|
style={styles.brandLink}
|
||||||
|
onMouseOver={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '#edae49';
|
||||||
|
e.currentTarget.style.color = '#2e4057';
|
||||||
|
}}
|
||||||
|
onMouseOut={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'white';
|
||||||
|
e.currentTarget.style.color = '#083d77';
|
||||||
|
}}
|
||||||
|
>Mercedes</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
src/app/components/SearchBar.tsx
Normal file
103
src/app/components/SearchBar.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, FormEvent } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
type SearchBarProps = {
|
||||||
|
initialQuery?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SearchBar({ initialQuery = '' }: SearchBarProps) {
|
||||||
|
const [query, setQuery] = useState(initialQuery);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (query.trim()) {
|
||||||
|
router.push(`/results?query=${encodeURIComponent(query)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
searchContainer: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '42rem',
|
||||||
|
margin: '0 auto 1.5rem'
|
||||||
|
},
|
||||||
|
searchForm: {
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative'
|
||||||
|
},
|
||||||
|
searchInput: {
|
||||||
|
width: '100%',
|
||||||
|
padding: '1.2rem 1.5rem',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
border: '2px solid transparent',
|
||||||
|
borderRadius: '30px',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
|
outline: 'none',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
color: '#2e4057'
|
||||||
|
},
|
||||||
|
searchButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '12px',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
width: '45px',
|
||||||
|
height: '45px',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: '#083d77',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '50%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.searchContainer}>
|
||||||
|
<form onSubmit={handleSubmit} style={styles.searchForm}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search for electric vehicles..."
|
||||||
|
style={styles.searchInput}
|
||||||
|
aria-label="Search for electric vehicles"
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.style.boxShadow = '0 6px 16px rgba(8, 61, 119, 0.2)';
|
||||||
|
e.target.style.borderColor = '#083d77';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)';
|
||||||
|
e.target.style.borderColor = 'transparent';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={styles.searchButton}
|
||||||
|
aria-label="Search"
|
||||||
|
onMouseOver={(e) => {
|
||||||
|
e.currentTarget.style.color = '#2e4057';
|
||||||
|
e.currentTarget.style.transform = 'translateY(-50%) scale(1.05)';
|
||||||
|
}}
|
||||||
|
onMouseOut={(e) => {
|
||||||
|
e.currentTarget.style.color = '#083d77';
|
||||||
|
e.currentTarget.style.transform = 'translateY(-50%)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,57 +1,38 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, FormEvent } from 'react';
|
import SearchBar from './components/SearchBar';
|
||||||
import { useRouter } from 'next/navigation';
|
import BrandList from './components/BrandList';
|
||||||
import Link from 'next/link';
|
|
||||||
import styles from './home.module.css';
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [query, setQuery] = useState('');
|
const styles = {
|
||||||
const router = useRouter();
|
container: {
|
||||||
|
minHeight: '100vh',
|
||||||
const handleSubmit = (e: FormEvent) => {
|
display: 'flex',
|
||||||
e.preventDefault();
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
if (query.trim()) {
|
justifyContent: 'center',
|
||||||
router.push(`/results?query=${encodeURIComponent(query)}`);
|
padding: '2rem',
|
||||||
|
background: 'linear-gradient(135deg, #ffffff, rgba(8, 61, 119, 0.05))'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: '3.5rem',
|
||||||
|
fontWeight: '800',
|
||||||
|
marginBottom: '2.5rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
letterSpacing: '-1px',
|
||||||
|
background: 'linear-gradient(90deg, #083d77 0%, #2e4057 35%, #d1495b 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
display: 'inline-block'
|
||||||
}
|
}
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.container}>
|
<main style={styles.container}>
|
||||||
<h1 className={styles.title}>
|
<h1 style={styles.title}>E-WIKI</h1>
|
||||||
<span className={styles.gradientText}>E-WIKI</span>
|
<SearchBar />
|
||||||
</h1>
|
<BrandList />
|
||||||
|
|
||||||
<div className={styles.searchContainer}>
|
|
||||||
<form onSubmit={handleSubmit} className={styles.searchForm}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
placeholder="Search for electric vehicles..."
|
|
||||||
className={styles.searchInput}
|
|
||||||
aria-label="Search for electric vehicles"
|
|
||||||
/>
|
|
||||||
<button type="submit" className={styles.searchButton}>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style={{ marginRight: '8px' }}>
|
|
||||||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
|
||||||
</svg>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.brandSection}>
|
|
||||||
<p>Popular brands</p>
|
|
||||||
<div className={styles.brandList}>
|
|
||||||
<Link href="/results?brand=1" className={styles.brandLink}>Tesla</Link>
|
|
||||||
<Link href="/results?brand=2" className={styles.brandLink}>BMW</Link>
|
|
||||||
<Link href="/results?brand=3" className={styles.brandLink}>Toyota</Link>
|
|
||||||
<Link href="/results?brand=4" className={styles.brandLink}>Audi</Link>
|
|
||||||
<Link href="/results?brand=5" className={styles.brandLink}>Mercedes</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,16 +2,33 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
|
||||||
import { CarService } from '../services/carService';
|
import { CarService } from '../services/carService';
|
||||||
import { Brand, CarModel, CarRevision } from '../../backend/models';
|
import { CarModel, CarRevision, Brand } from '../../backend/models';
|
||||||
|
import SearchBar from '../components/SearchBar';
|
||||||
|
import styles from './results.module.css';
|
||||||
|
|
||||||
|
// Extended interfaces for the frontend display
|
||||||
|
interface DisplayCarModel extends CarModel {
|
||||||
|
brand?: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DisplayCarRevision extends CarRevision {
|
||||||
|
baseModel?: {
|
||||||
|
brand?: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function Results() {
|
export default function Results() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [models, setModels] = useState<CarModel[]>([]);
|
const [models, setModels] = useState<DisplayCarModel[]>([]);
|
||||||
const [revisions, setRevisions] = useState<CarRevision[]>([]);
|
const [revisions, setRevisions] = useState<DisplayCarRevision[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const query = searchParams.get('query') || '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchResults = async () => {
|
const fetchResults = async () => {
|
||||||
@ -56,71 +73,72 @@ export default function Results() {
|
|||||||
fetchResults();
|
fetchResults();
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
|
// Helper function to generate a unique key
|
||||||
|
const getModelKey = (model: DisplayCarModel) => {
|
||||||
|
return model._id?.toString() || model.id || `model-${model.name}-${model.productionStartYear}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRevisionKey = (revision: DisplayCarRevision) => {
|
||||||
|
return revision._id?.toString() || revision.id || `revision-${revision.name}-${revision.releaseYear}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen p-8 bg-gray-50">
|
<main className={styles.container}>
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className={styles.content}>
|
||||||
<div className="flex justify-between items-center mb-8">
|
<SearchBar initialQuery={query} />
|
||||||
<h1 className="text-3xl font-bold">Search Results</h1>
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
New Search
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center items-center py-12">
|
<div className={styles.loader}>
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
<div className={styles.spinner}></div>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">
|
<div className={styles.errorMessage}>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : models.length === 0 && revisions.length === 0 ? (
|
) : models.length === 0 && revisions.length === 0 ? (
|
||||||
<div className="bg-white p-8 rounded-lg shadow-md text-center">
|
<div className={styles.emptyResults}>
|
||||||
<h2 className="text-2xl font-semibold mb-4">No results found</h2>
|
<h2 className={styles.noResultsTitle}>No results found</h2>
|
||||||
<p className="text-gray-600">
|
<p className={styles.noResultsText}>
|
||||||
Try adjusting your search criteria to find more cars.
|
Try adjusting your search criteria to find more cars.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-8">
|
<div>
|
||||||
{models.length > 0 && (
|
{models.length > 0 && (
|
||||||
<div>
|
<div className={styles.sectionContent}>
|
||||||
<h2 className="text-2xl font-semibold mb-4">Car Models</h2>
|
<h2 className={styles.sectionTitle}>Car Models</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className={styles.grid}>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<div
|
<div
|
||||||
key={model.id}
|
key={getModelKey(model)}
|
||||||
className="bg-white rounded-lg shadow-md overflow-hidden"
|
className={styles.card}
|
||||||
>
|
>
|
||||||
<div className="h-48 bg-gray-200 relative">
|
<div className={styles.cardImage}>
|
||||||
{model.image ? (
|
{model.image ? (
|
||||||
<img
|
<img
|
||||||
src={model.image}
|
src={model.image}
|
||||||
alt={model.name}
|
alt={model.name}
|
||||||
className="w-full h-full object-cover"
|
className={styles.image}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-gray-500">
|
<div className={styles.noImage}>
|
||||||
No image available
|
No image available
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className={styles.cardContent}>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className={styles.cardHeader}>
|
||||||
<h3 className="text-xl font-bold">{model.name}</h3>
|
<h3 className={styles.cardTitle}>{model.name}</h3>
|
||||||
<span className="text-sm font-medium text-blue-600">
|
<span className={styles.brandName}>
|
||||||
{model.brand?.name}
|
{model.brand?.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 text-sm mb-2">{model.description}</p>
|
<p className={styles.cardDescription}>{model.description}</p>
|
||||||
<div className="flex justify-between items-center text-sm">
|
<div className={styles.cardFooter}>
|
||||||
<span className="bg-gray-100 px-2 py-1 rounded">
|
<span className={styles.category}>
|
||||||
{model.category}
|
{model.category}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span className={styles.years}>
|
||||||
Since {model.productionStartYear}
|
Since {model.productionStartYear}
|
||||||
{model.productionEndYear
|
{model.productionEndYear
|
||||||
? ` - ${model.productionEndYear}`
|
? ` - ${model.productionEndYear}`
|
||||||
@ -135,63 +153,63 @@ export default function Results() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{revisions.length > 0 && (
|
{revisions.length > 0 && (
|
||||||
<div>
|
<div className={styles.sectionContent}>
|
||||||
<h2 className="text-2xl font-semibold mb-4">Car Revisions</h2>
|
<h2 className={styles.sectionTitle}>Car Revisions</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className={styles.grid}>
|
||||||
{revisions.map((revision) => (
|
{revisions.map((revision) => (
|
||||||
<div
|
<div
|
||||||
key={revision.id}
|
key={getRevisionKey(revision)}
|
||||||
className="bg-white rounded-lg shadow-md overflow-hidden"
|
className={styles.card}
|
||||||
>
|
>
|
||||||
<div className="h-48 bg-gray-200 relative">
|
<div className={styles.cardImage}>
|
||||||
{revision.images && revision.images.length > 0 ? (
|
{revision.images && revision.images.length > 0 ? (
|
||||||
<img
|
<img
|
||||||
src={revision.images[0]}
|
src={revision.images[0]}
|
||||||
alt={revision.name}
|
alt={revision.name}
|
||||||
className="w-full h-full object-cover"
|
className={styles.image}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-gray-500">
|
<div className={styles.noImage}>
|
||||||
No image available
|
No image available
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className={styles.cardContent}>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className={styles.cardHeader}>
|
||||||
<h3 className="text-xl font-bold">{revision.name}</h3>
|
<h3 className={styles.cardTitle}>{revision.name}</h3>
|
||||||
<span className="text-sm font-medium text-blue-600">
|
<span className={styles.brandName}>
|
||||||
{revision.baseModel?.brand?.name}
|
{revision.baseModel?.brand?.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 mb-2 text-sm">
|
<div className={styles.specGrid}>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Engine: </span>
|
<span className={styles.specLabel}>Engine: </span>
|
||||||
{revision.engineTypes?.join(', ')}
|
{revision.engineTypes?.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Power: </span>
|
<span className={styles.specLabel}>Power: </span>
|
||||||
{revision.horsePower} HP
|
{revision.horsePower} HP
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">0-100 km/h: </span>
|
<span className={styles.specLabel}>0-100 km/h: </span>
|
||||||
{revision.acceleration0To100}s
|
{revision.acceleration0To100}s
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Top Speed: </span>
|
<span className={styles.specLabel}>Top Speed: </span>
|
||||||
{revision.topSpeed} km/h
|
{revision.topSpeed} km/h
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
<div className={styles.tagContainer}>
|
||||||
{revision.features?.slice(0, 3).map((feature, index) => (
|
{revision.features?.slice(0, 3).map((feature, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={`${getRevisionKey(revision)}-feature-${index}`}
|
||||||
className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded"
|
className={styles.tag}
|
||||||
>
|
>
|
||||||
{feature}
|
{feature}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{revision.features && revision.features.length > 3 && (
|
{revision.features && revision.features.length > 3 && (
|
||||||
<span className="text-xs text-gray-500">
|
<span className={styles.tagCount}>
|
||||||
+{revision.features.length - 3} more
|
+{revision.features.length - 3} more
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
.resultsContainer {
|
/* Results page specific styles */
|
||||||
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, var(--background), rgba(8, 61, 119, 0.05));
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background-color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentContainer {
|
|
||||||
max-width: 80rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@ -14,26 +10,38 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultsTitle {
|
.title {
|
||||||
font-size: 1.875rem;
|
font-size: 2.5rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
|
color: var(--color-charcoal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.backButton {
|
.backButton {
|
||||||
background-color: #3b82f6;
|
background-color: var(--color-yale-blue);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.6rem 1.2rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 25px;
|
||||||
transition: background-color 0.2s;
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backButton:hover {
|
.backButton:hover {
|
||||||
background-color: #2563eb;
|
background-color: var(--color-charcoal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadingContainer {
|
.content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -41,11 +49,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
border-radius: 50%;
|
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
border: 0.25rem solid rgba(59, 130, 246, 0.1);
|
border: 0.25rem solid rgba(8, 61, 119, 0.1);
|
||||||
border-top-color: #3b82f6;
|
border-top: 0.25rem solid var(--color-yale-blue);
|
||||||
|
border-radius: 50%;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,56 +62,64 @@
|
|||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorContainer {
|
.errorMessage {
|
||||||
background-color: #fee2e2;
|
background-color: #fde8e8;
|
||||||
border: 1px solid #f87171;
|
border: 1px solid #f8b4b4;
|
||||||
color: #b91c1c;
|
color: #9b1c1c;
|
||||||
padding: 0.75rem 1rem;
|
padding: 1rem;
|
||||||
border-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyResultsContainer {
|
|
||||||
background-color: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyResultsTitle {
|
.noResults {
|
||||||
|
background-color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyResults {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noResultsTitle {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
color: var(--color-charcoal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyResultsMessage {
|
.noResultsText {
|
||||||
color: #6b7280;
|
color: #4b5563;
|
||||||
}
|
|
||||||
|
|
||||||
.resultsSection {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
font-size: 1.5rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--color-charcoal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardsGrid {
|
.sectionContent {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(1, 1fr);
|
grid-template-columns: repeat(1, 1fr);
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.cardsGrid {
|
.grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.cardsGrid {
|
.grid {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,23 +127,29 @@
|
|||||||
.card {
|
.card {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardImageContainer {
|
.card:hover {
|
||||||
height: 12rem;
|
transform: translateY(-4px);
|
||||||
background-color: #e5e7eb;
|
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardImage {
|
.cardImage {
|
||||||
|
height: 12rem;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noImageContainer {
|
.noImage {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -148,17 +170,18 @@
|
|||||||
|
|
||||||
.cardTitle {
|
.cardTitle {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
|
color: var(--color-charcoal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brandName {
|
.brandName {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #3b82f6;
|
color: var(--color-yale-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardDescription {
|
.cardDescription {
|
||||||
color: #6b7280;
|
color: #4b5563;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
@ -174,9 +197,14 @@
|
|||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
color: #4b5563;
|
||||||
}
|
}
|
||||||
|
|
||||||
.specsGrid {
|
.years {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@ -186,16 +214,17 @@
|
|||||||
|
|
||||||
.specLabel {
|
.specLabel {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: var(--color-charcoal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.featuresList {
|
.tagContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.featureTag {
|
.tag {
|
||||||
background-color: #dbeafe;
|
background-color: #dbeafe;
|
||||||
color: #1e40af;
|
color: #1e40af;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
@ -203,7 +232,7 @@
|
|||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moreFeatures {
|
.tagCount {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import { CarModel } from './CarModel';
|
import { CarModel } from './CarModel';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
export interface Brand {
|
export interface Brand {
|
||||||
id: string;
|
_id?: ObjectId;
|
||||||
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -10,4 +12,3 @@ export interface Brand {
|
|||||||
website: string;
|
website: string;
|
||||||
carModels?: CarModel[];
|
carModels?: CarModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { Brand } from './Brand';
|
import { Brand } from './Brand';
|
||||||
import { CarRevision } from './CarRevision';
|
import { CarRevision } from './CarRevision';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
export interface CarModel {
|
export interface CarModel {
|
||||||
id: string;
|
_id?: ObjectId;
|
||||||
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
brand: Brand;
|
|
||||||
productionStartYear: number;
|
productionStartYear: number;
|
||||||
productionEndYear?: number;
|
productionEndYear?: number;
|
||||||
category?: string;
|
category?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
revisions?: CarRevision[];
|
revisions?: CarRevision[];
|
||||||
|
brandId?: ObjectId;
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { CarModel } from './CarModel';
|
import { CarModel } from './CarModel';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
export interface Dimensions {
|
export interface Dimensions {
|
||||||
length: number;
|
length: number;
|
||||||
@ -8,9 +9,9 @@ export interface Dimensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CarRevision {
|
export interface CarRevision {
|
||||||
id: string;
|
_id?: ObjectId;
|
||||||
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
baseModel: CarModel;
|
|
||||||
releaseYear: number;
|
releaseYear: number;
|
||||||
engineTypes: string[];
|
engineTypes: string[];
|
||||||
horsePower: number;
|
horsePower: number;
|
||||||
@ -22,4 +23,5 @@ export interface CarRevision {
|
|||||||
weight: number;
|
weight: number;
|
||||||
features: string[];
|
features: string[];
|
||||||
images: string[];
|
images: string[];
|
||||||
|
modelId?: ObjectId;
|
||||||
}
|
}
|
||||||
@ -1,512 +1,540 @@
|
|||||||
import { Brand, CarModel, CarRevision } from '../models';
|
import { Brand, CarModel, CarRevision } from '../models';
|
||||||
|
import { MongoClient, Db, Collection, ObjectId } from 'mongodb';
|
||||||
|
|
||||||
// Dummy brands
|
|
||||||
const brands: Brand[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Tesla',
|
|
||||||
logo: 'https://example.com/tesla-logo.png',
|
|
||||||
description: 'American electric vehicle and clean energy company',
|
|
||||||
foundedYear: 2003,
|
|
||||||
headquarters: 'Palo Alto, California, United States',
|
|
||||||
website: 'https://www.tesla.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'BMW',
|
|
||||||
logo: 'https://example.com/bmw-logo.png',
|
|
||||||
description: 'German luxury automobile and motorcycle manufacturer',
|
|
||||||
foundedYear: 1916,
|
|
||||||
headquarters: 'Munich, Germany',
|
|
||||||
website: 'https://www.bmw.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Toyota',
|
|
||||||
logo: 'https://example.com/toyota-logo.png',
|
|
||||||
description: 'Japanese multinational automotive manufacturer',
|
|
||||||
foundedYear: 1937,
|
|
||||||
headquarters: 'Toyota City, Japan',
|
|
||||||
website: 'https://www.toyota.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'Audi',
|
|
||||||
logo: 'https://example.com/audi-logo.png',
|
|
||||||
description: 'German luxury automobile manufacturer',
|
|
||||||
foundedYear: 1909,
|
|
||||||
headquarters: 'Ingolstadt, Germany',
|
|
||||||
website: 'https://www.audi.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
name: 'Mercedes-Benz',
|
|
||||||
logo: 'https://example.com/mercedes-logo.png',
|
|
||||||
description: 'German global automobile marque and a division of Daimler AG',
|
|
||||||
foundedYear: 1926,
|
|
||||||
headquarters: 'Stuttgart, Germany',
|
|
||||||
website: 'https://www.mercedes-benz.com',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Dummy car models (without revisions yet)
|
|
||||||
const carModels: CarModel[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Model S',
|
|
||||||
brand: brands[0], // Tesla
|
|
||||||
productionStartYear: 2012,
|
|
||||||
category: 'Sedan',
|
|
||||||
description: 'All-electric five-door liftback sedan',
|
|
||||||
image: 'https://example.com/tesla-model-s.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Model 3',
|
|
||||||
brand: brands[0], // Tesla
|
|
||||||
productionStartYear: 2017,
|
|
||||||
category: 'Sedan',
|
|
||||||
description: 'All-electric four-door sedan',
|
|
||||||
image: 'https://example.com/tesla-model-3.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: '3 Series',
|
|
||||||
brand: brands[1], // BMW
|
|
||||||
productionStartYear: 1975,
|
|
||||||
category: 'Sedan',
|
|
||||||
description: 'Compact executive car',
|
|
||||||
image: 'https://example.com/bmw-3-series.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'X5',
|
|
||||||
brand: brands[1], // BMW
|
|
||||||
productionStartYear: 1999,
|
|
||||||
category: 'SUV',
|
|
||||||
description: 'Mid-size luxury SUV',
|
|
||||||
image: 'https://example.com/bmw-x5.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
name: 'Camry',
|
|
||||||
brand: brands[2], // Toyota
|
|
||||||
productionStartYear: 1982,
|
|
||||||
category: 'Sedan',
|
|
||||||
description: 'Mid-size car',
|
|
||||||
image: 'https://example.com/toyota-camry.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
name: 'Prius',
|
|
||||||
brand: brands[2], // Toyota
|
|
||||||
productionStartYear: 1997,
|
|
||||||
category: 'Hatchback',
|
|
||||||
description: 'Hybrid electric mid-size car',
|
|
||||||
image: 'https://example.com/toyota-prius.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
name: 'A4',
|
|
||||||
brand: brands[3], // Audi
|
|
||||||
productionStartYear: 1994,
|
|
||||||
category: 'Sedan',
|
|
||||||
description: 'Compact executive car',
|
|
||||||
image: 'https://example.com/audi-a4.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
name: 'Q7',
|
|
||||||
brand: brands[3], // Audi
|
|
||||||
productionStartYear: 2005,
|
|
||||||
category: 'SUV',
|
|
||||||
description: 'Full-size luxury crossover SUV',
|
|
||||||
image: 'https://example.com/audi-q7.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '9',
|
|
||||||
name: 'C-Class',
|
|
||||||
brand: brands[4], // Mercedes-Benz
|
|
||||||
productionStartYear: 1993,
|
|
||||||
category: 'Sedan',
|
|
||||||
description: 'Compact executive car',
|
|
||||||
image: 'https://example.com/mercedes-c-class.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '10',
|
|
||||||
name: 'GLE',
|
|
||||||
brand: brands[4], // Mercedes-Benz
|
|
||||||
productionStartYear: 2015,
|
|
||||||
category: 'SUV',
|
|
||||||
description: 'Mid-size luxury crossover SUV',
|
|
||||||
image: 'https://example.com/mercedes-gle.jpg',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Dummy car revisions
|
|
||||||
const carRevisions: CarRevision[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Model S Long Range Plus',
|
|
||||||
baseModel: carModels[0], // Tesla Model S
|
|
||||||
releaseYear: 2020,
|
|
||||||
engineTypes: ['Electric'],
|
|
||||||
horsePower: 670,
|
|
||||||
torque: 850,
|
|
||||||
topSpeed: 250,
|
|
||||||
acceleration0To100: 3.1,
|
|
||||||
fuelConsumption: 0,
|
|
||||||
dimensions: {
|
|
||||||
length: 4970,
|
|
||||||
width: 1964,
|
|
||||||
height: 1445,
|
|
||||||
wheelbase: 2960,
|
|
||||||
},
|
|
||||||
weight: 2250,
|
|
||||||
features: ['Autopilot', 'Premium Interior', 'All-Wheel Drive'],
|
|
||||||
images: ['https://example.com/tesla-model-s-2020-1.jpg', 'https://example.com/tesla-model-s-2020-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Model S Plaid',
|
|
||||||
baseModel: carModels[0], // Tesla Model S
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Electric'],
|
|
||||||
horsePower: 1020,
|
|
||||||
torque: 1050,
|
|
||||||
topSpeed: 322,
|
|
||||||
acceleration0To100: 2.1,
|
|
||||||
fuelConsumption: 0,
|
|
||||||
dimensions: {
|
|
||||||
length: 4970,
|
|
||||||
width: 1964,
|
|
||||||
height: 1445,
|
|
||||||
wheelbase: 2960,
|
|
||||||
},
|
|
||||||
weight: 2300,
|
|
||||||
features: ['Enhanced Autopilot', 'Yoke Steering', 'All-Wheel Drive', 'New Interior Design'],
|
|
||||||
images: ['https://example.com/tesla-model-s-plaid-1.jpg', 'https://example.com/tesla-model-s-plaid-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Model 3 Standard Range Plus',
|
|
||||||
baseModel: carModels[1], // Tesla Model 3
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Electric'],
|
|
||||||
horsePower: 283,
|
|
||||||
torque: 450,
|
|
||||||
topSpeed: 225,
|
|
||||||
acceleration0To100: 5.6,
|
|
||||||
fuelConsumption: 0,
|
|
||||||
dimensions: {
|
|
||||||
length: 4694,
|
|
||||||
width: 1849,
|
|
||||||
height: 1443,
|
|
||||||
wheelbase: 2875,
|
|
||||||
},
|
|
||||||
weight: 1750,
|
|
||||||
features: ['Basic Autopilot', 'Standard Interior', 'Rear-Wheel Drive'],
|
|
||||||
images: ['https://example.com/tesla-model-3-standard-1.jpg', 'https://example.com/tesla-model-3-standard-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: '330i Sedan',
|
|
||||||
baseModel: carModels[2], // BMW 3 Series
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Gasoline'],
|
|
||||||
horsePower: 255,
|
|
||||||
torque: 400,
|
|
||||||
topSpeed: 209,
|
|
||||||
acceleration0To100: 5.6,
|
|
||||||
fuelConsumption: 7.1,
|
|
||||||
dimensions: {
|
|
||||||
length: 4709,
|
|
||||||
width: 1827,
|
|
||||||
height: 1435,
|
|
||||||
wheelbase: 2851,
|
|
||||||
},
|
|
||||||
weight: 1620,
|
|
||||||
features: ['LED Headlights', 'iDrive Infotainment System', 'Leather Seats'],
|
|
||||||
images: ['https://example.com/bmw-330i-1.jpg', 'https://example.com/bmw-330i-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
name: 'M340i Sedan',
|
|
||||||
baseModel: carModels[2], // BMW 3 Series
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Gasoline'],
|
|
||||||
horsePower: 382,
|
|
||||||
torque: 500,
|
|
||||||
topSpeed: 250,
|
|
||||||
acceleration0To100: 4.4,
|
|
||||||
fuelConsumption: 8.0,
|
|
||||||
dimensions: {
|
|
||||||
length: 4709,
|
|
||||||
width: 1827,
|
|
||||||
height: 1435,
|
|
||||||
wheelbase: 2851,
|
|
||||||
},
|
|
||||||
weight: 1670,
|
|
||||||
features: ['M Sport Differential', 'M Sport Brakes', 'Adaptive M Suspension'],
|
|
||||||
images: ['https://example.com/bmw-m340i-1.jpg', 'https://example.com/bmw-m340i-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
name: 'X5 xDrive40i',
|
|
||||||
baseModel: carModels[3], // BMW X5
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Gasoline'],
|
|
||||||
horsePower: 335,
|
|
||||||
torque: 450,
|
|
||||||
topSpeed: 243,
|
|
||||||
acceleration0To100: 5.5,
|
|
||||||
fuelConsumption: 9.2,
|
|
||||||
dimensions: {
|
|
||||||
length: 4922,
|
|
||||||
width: 2004,
|
|
||||||
height: 1745,
|
|
||||||
wheelbase: 2975,
|
|
||||||
},
|
|
||||||
weight: 2260,
|
|
||||||
features: ['Panoramic Roof', 'Head-Up Display', 'Gesture Control'],
|
|
||||||
images: ['https://example.com/bmw-x5-xdrive40i-1.jpg', 'https://example.com/bmw-x5-xdrive40i-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
name: 'Camry LE',
|
|
||||||
baseModel: carModels[4], // Toyota Camry
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Gasoline'],
|
|
||||||
horsePower: 203,
|
|
||||||
torque: 250,
|
|
||||||
topSpeed: 210,
|
|
||||||
acceleration0To100: 8.1,
|
|
||||||
fuelConsumption: 7.6,
|
|
||||||
dimensions: {
|
|
||||||
length: 4880,
|
|
||||||
width: 1840,
|
|
||||||
height: 1445,
|
|
||||||
wheelbase: 2825,
|
|
||||||
},
|
|
||||||
weight: 1580,
|
|
||||||
features: ['Toyota Safety Sense', 'Apple CarPlay', 'Android Auto'],
|
|
||||||
images: ['https://example.com/toyota-camry-le-1.jpg', 'https://example.com/toyota-camry-le-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
name: 'Camry Hybrid',
|
|
||||||
baseModel: carModels[4], // Toyota Camry
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Hybrid'],
|
|
||||||
horsePower: 208,
|
|
||||||
torque: 220,
|
|
||||||
topSpeed: 180,
|
|
||||||
acceleration0To100: 7.8,
|
|
||||||
fuelConsumption: 4.2,
|
|
||||||
dimensions: {
|
|
||||||
length: 4880,
|
|
||||||
width: 1840,
|
|
||||||
height: 1445,
|
|
||||||
wheelbase: 2825,
|
|
||||||
},
|
|
||||||
weight: 1680,
|
|
||||||
features: ['Regenerative Braking', 'EV Mode', 'Energy Monitor'],
|
|
||||||
images: ['https://example.com/toyota-camry-hybrid-1.jpg', 'https://example.com/toyota-camry-hybrid-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '9',
|
|
||||||
name: 'Prius Prime',
|
|
||||||
baseModel: carModels[5], // Toyota Prius
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Plug-in Hybrid'],
|
|
||||||
horsePower: 121,
|
|
||||||
torque: 142,
|
|
||||||
topSpeed: 165,
|
|
||||||
acceleration0To100: 10.5,
|
|
||||||
fuelConsumption: 1.8,
|
|
||||||
dimensions: {
|
|
||||||
length: 4645,
|
|
||||||
width: 1760,
|
|
||||||
height: 1470,
|
|
||||||
wheelbase: 2700,
|
|
||||||
},
|
|
||||||
weight: 1530,
|
|
||||||
features: ['Electric Range of 40 km', 'Touch-sensitive controls', 'Quad-LED projector headlights'],
|
|
||||||
images: ['https://example.com/toyota-prius-prime-1.jpg', 'https://example.com/toyota-prius-prime-2.jpg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '10',
|
|
||||||
name: 'A4 Prestige',
|
|
||||||
baseModel: carModels[6], // Audi A4
|
|
||||||
releaseYear: 2021,
|
|
||||||
engineTypes: ['Gasoline'],
|
|
||||||
horsePower: 261,
|
|
||||||
torque: 370,
|
|
||||||
topSpeed: 210,
|
|
||||||
acceleration0To100: 5.5,
|
|
||||||
fuelConsumption: 7.5,
|
|
||||||
dimensions: {
|
|
||||||
length: 4762,
|
|
||||||
width: 1847,
|
|
||||||
height: 1435,
|
|
||||||
wheelbase: 2820,
|
|
||||||
},
|
|
||||||
weight: 1640,
|
|
||||||
features: ['Audi Virtual Cockpit', 'Bang & Olufsen Sound System', 'Adaptive Cruise Control'],
|
|
||||||
images: ['https://example.com/audi-a4-prestige-1.jpg', 'https://example.com/audi-a4-prestige-2.jpg'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Connect revisions to models
|
|
||||||
const connectRevisionsToModels = () => {
|
|
||||||
// Tesla Model S revisions
|
|
||||||
carModels[0].revisions = [carRevisions[0], carRevisions[1]];
|
|
||||||
|
|
||||||
// Tesla Model 3 revisions
|
|
||||||
carModels[1].revisions = [carRevisions[2]];
|
|
||||||
|
|
||||||
// BMW 3 Series revisions
|
|
||||||
carModels[2].revisions = [carRevisions[3], carRevisions[4]];
|
|
||||||
|
|
||||||
// BMW X5 revisions
|
|
||||||
carModels[3].revisions = [carRevisions[5]];
|
|
||||||
|
|
||||||
// Toyota Camry revisions
|
|
||||||
carModels[4].revisions = [carRevisions[6], carRevisions[7]];
|
|
||||||
|
|
||||||
// Toyota Prius revisions
|
|
||||||
carModels[5].revisions = [carRevisions[8]];
|
|
||||||
|
|
||||||
// Audi A4 revisions
|
|
||||||
carModels[6].revisions = [carRevisions[9]];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connect car models to brands
|
|
||||||
const connectModelsToBrands = () => {
|
|
||||||
// Tesla models
|
|
||||||
brands[0].carModels = [carModels[0], carModels[1]];
|
|
||||||
|
|
||||||
// BMW models
|
|
||||||
brands[1].carModels = [carModels[2], carModels[3]];
|
|
||||||
|
|
||||||
// Toyota models
|
|
||||||
brands[2].carModels = [carModels[4], carModels[5]];
|
|
||||||
|
|
||||||
// Audi models
|
|
||||||
brands[3].carModels = [carModels[6], carModels[7]];
|
|
||||||
|
|
||||||
// Mercedes models
|
|
||||||
brands[4].carModels = [carModels[8], carModels[9]];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize connections
|
|
||||||
connectRevisionsToModels();
|
|
||||||
connectModelsToBrands();
|
|
||||||
|
|
||||||
// Car Repository Service
|
|
||||||
export class CarRepository {
|
export class CarRepository {
|
||||||
|
private client: MongoClient;
|
||||||
|
private db: Db | null = null;
|
||||||
|
private brandsCollection: Collection<Brand> | null = null;
|
||||||
|
private carModelsCollection: Collection<CarModel> | null = null;
|
||||||
|
private carRevisionsCollection: Collection<CarRevision> | null = null;
|
||||||
|
|
||||||
|
constructor(private mongoUrl: string = process.env.MONGODB_URI || 'mongodb://localhost:27017/evwiki') {
|
||||||
|
this.client = new MongoClient(this.mongoUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the database connection
|
||||||
|
*/
|
||||||
|
async connect(): Promise<void> {
|
||||||
|
if (!this.db) {
|
||||||
|
await this.client.connect();
|
||||||
|
this.db = this.client.db();
|
||||||
|
this.brandsCollection = this.db.collection<Brand>('brands');
|
||||||
|
this.carModelsCollection = this.db.collection<CarModel>('carModels');
|
||||||
|
this.carRevisionsCollection = this.db.collection<CarRevision>('carRevisions');
|
||||||
|
|
||||||
|
// Check if data exists, if not initialize with default data
|
||||||
|
const brandsCount = await this.brandsCollection.countDocuments();
|
||||||
|
if (brandsCount === 0) {
|
||||||
|
await this.initializeDefaultData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the database connection
|
||||||
|
*/
|
||||||
|
async disconnect(): Promise<void> {
|
||||||
|
if (this.client) {
|
||||||
|
await this.client.close();
|
||||||
|
this.db = null;
|
||||||
|
this.brandsCollection = null;
|
||||||
|
this.carModelsCollection = null;
|
||||||
|
this.carRevisionsCollection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the database with default data if empty
|
||||||
|
*/
|
||||||
|
private async initializeDefaultData(): Promise<void> {
|
||||||
|
if (!this.db || !this.brandsCollection || !this.carModelsCollection || !this.carRevisionsCollection) {
|
||||||
|
throw new Error('Database not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert default brands
|
||||||
|
await this.brandsCollection.insertMany([
|
||||||
|
{
|
||||||
|
name: 'Tesla',
|
||||||
|
logo: 'https://example.com/tesla-logo.png',
|
||||||
|
description: 'American electric vehicle and clean energy company',
|
||||||
|
foundedYear: 2003,
|
||||||
|
headquarters: 'Palo Alto, California, United States',
|
||||||
|
website: 'https://www.tesla.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'BMW',
|
||||||
|
logo: 'https://example.com/bmw-logo.png',
|
||||||
|
description: 'German luxury automobile and motorcycle manufacturer',
|
||||||
|
foundedYear: 1916,
|
||||||
|
headquarters: 'Munich, Germany',
|
||||||
|
website: 'https://www.bmw.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Toyota',
|
||||||
|
logo: 'https://example.com/toyota-logo.png',
|
||||||
|
description: 'Japanese multinational automotive manufacturer',
|
||||||
|
foundedYear: 1937,
|
||||||
|
headquarters: 'Toyota City, Japan',
|
||||||
|
website: 'https://www.toyota.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Audi',
|
||||||
|
logo: 'https://example.com/audi-logo.png',
|
||||||
|
description: 'German luxury automobile manufacturer',
|
||||||
|
foundedYear: 1909,
|
||||||
|
headquarters: 'Ingolstadt, Germany',
|
||||||
|
website: 'https://www.audi.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mercedes-Benz',
|
||||||
|
logo: 'https://example.com/mercedes-logo.png',
|
||||||
|
description: 'German global automobile marque and a division of Daimler AG',
|
||||||
|
foundedYear: 1926,
|
||||||
|
headquarters: 'Stuttgart, Germany',
|
||||||
|
website: 'https://www.mercedes-benz.com',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get the inserted brands to connect them with models later
|
||||||
|
const brands = await this.brandsCollection.find().toArray();
|
||||||
|
|
||||||
|
// Insert default car models
|
||||||
|
await this.carModelsCollection.insertMany([
|
||||||
|
{
|
||||||
|
name: 'Model S',
|
||||||
|
productionStartYear: 2012,
|
||||||
|
category: 'Sedan',
|
||||||
|
description: 'All-electric five-door liftback sedan',
|
||||||
|
image: 'https://example.com/tesla-model-s.jpg',
|
||||||
|
brandId: brands[0]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Model 3',
|
||||||
|
productionStartYear: 2017,
|
||||||
|
category: 'Sedan',
|
||||||
|
description: 'All-electric four-door sedan',
|
||||||
|
image: 'https://example.com/tesla-model-3.jpg',
|
||||||
|
brandId: brands[0]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '3 Series',
|
||||||
|
productionStartYear: 1975,
|
||||||
|
category: 'Sedan',
|
||||||
|
description: 'Compact executive car',
|
||||||
|
image: 'https://example.com/bmw-3-series.jpg',
|
||||||
|
brandId: brands[1]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'X5',
|
||||||
|
productionStartYear: 1999,
|
||||||
|
category: 'SUV',
|
||||||
|
description: 'Mid-size luxury SUV',
|
||||||
|
image: 'https://example.com/bmw-x5.jpg',
|
||||||
|
brandId: brands[1]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Camry',
|
||||||
|
productionStartYear: 1982,
|
||||||
|
category: 'Sedan',
|
||||||
|
description: 'Mid-size car',
|
||||||
|
image: 'https://example.com/toyota-camry.jpg',
|
||||||
|
brandId: brands[2]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Prius',
|
||||||
|
productionStartYear: 1997,
|
||||||
|
category: 'Hatchback',
|
||||||
|
description: 'Hybrid electric mid-size car',
|
||||||
|
image: 'https://example.com/toyota-prius.jpg',
|
||||||
|
brandId: brands[2]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'A4',
|
||||||
|
productionStartYear: 1994,
|
||||||
|
category: 'Sedan',
|
||||||
|
description: 'Compact executive car',
|
||||||
|
image: 'https://example.com/audi-a4.jpg',
|
||||||
|
brandId: brands[3]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Q7',
|
||||||
|
productionStartYear: 2005,
|
||||||
|
category: 'SUV',
|
||||||
|
description: 'Full-size luxury crossover SUV',
|
||||||
|
image: 'https://example.com/audi-q7.jpg',
|
||||||
|
brandId: brands[3]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'C-Class',
|
||||||
|
productionStartYear: 1993,
|
||||||
|
category: 'Sedan',
|
||||||
|
description: 'Compact executive car',
|
||||||
|
image: 'https://example.com/mercedes-c-class.jpg',
|
||||||
|
brandId: brands[4]._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GLE',
|
||||||
|
productionStartYear: 2015,
|
||||||
|
category: 'SUV',
|
||||||
|
description: 'Mid-size luxury crossover SUV',
|
||||||
|
image: 'https://example.com/mercedes-gle.jpg',
|
||||||
|
brandId: brands[4]._id,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get the inserted models to connect them with revisions
|
||||||
|
const models = await this.carModelsCollection.find().toArray();
|
||||||
|
|
||||||
|
// Find models by name for easy reference
|
||||||
|
const findModelByName = (name: string) => models.find(model => model.name === name);
|
||||||
|
|
||||||
|
// Insert default car revisions
|
||||||
|
await this.carRevisionsCollection.insertMany([
|
||||||
|
{
|
||||||
|
name: 'Model S Long Range Plus',
|
||||||
|
releaseYear: 2020,
|
||||||
|
engineTypes: ['Electric'],
|
||||||
|
horsePower: 670,
|
||||||
|
torque: 850,
|
||||||
|
topSpeed: 250,
|
||||||
|
acceleration0To100: 3.1,
|
||||||
|
fuelConsumption: 0,
|
||||||
|
dimensions: {
|
||||||
|
length: 4970,
|
||||||
|
width: 1964,
|
||||||
|
height: 1445,
|
||||||
|
wheelbase: 2960,
|
||||||
|
},
|
||||||
|
weight: 2250,
|
||||||
|
features: ['Autopilot', 'Premium Interior', 'All-Wheel Drive'],
|
||||||
|
images: ['https://example.com/tesla-model-s-2020-1.jpg', 'https://example.com/tesla-model-s-2020-2.jpg'],
|
||||||
|
modelId: findModelByName('Model S')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Model S Plaid',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Electric'],
|
||||||
|
horsePower: 1020,
|
||||||
|
torque: 1050,
|
||||||
|
topSpeed: 322,
|
||||||
|
acceleration0To100: 2.1,
|
||||||
|
fuelConsumption: 0,
|
||||||
|
dimensions: {
|
||||||
|
length: 4970,
|
||||||
|
width: 1964,
|
||||||
|
height: 1445,
|
||||||
|
wheelbase: 2960,
|
||||||
|
},
|
||||||
|
weight: 2300,
|
||||||
|
features: ['Enhanced Autopilot', 'Yoke Steering', 'All-Wheel Drive', 'New Interior Design'],
|
||||||
|
images: ['https://example.com/tesla-model-s-plaid-1.jpg', 'https://example.com/tesla-model-s-plaid-2.jpg'],
|
||||||
|
modelId: findModelByName('Model S')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Model 3 Standard Range Plus',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Electric'],
|
||||||
|
horsePower: 283,
|
||||||
|
torque: 450,
|
||||||
|
topSpeed: 225,
|
||||||
|
acceleration0To100: 5.6,
|
||||||
|
fuelConsumption: 0,
|
||||||
|
dimensions: {
|
||||||
|
length: 4694,
|
||||||
|
width: 1849,
|
||||||
|
height: 1443,
|
||||||
|
wheelbase: 2875,
|
||||||
|
},
|
||||||
|
weight: 1750,
|
||||||
|
features: ['Basic Autopilot', 'Standard Interior', 'Rear-Wheel Drive'],
|
||||||
|
images: ['https://example.com/tesla-model-3-standard-1.jpg', 'https://example.com/tesla-model-3-standard-2.jpg'],
|
||||||
|
modelId: findModelByName('Model 3')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '330i Sedan',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Gasoline'],
|
||||||
|
horsePower: 255,
|
||||||
|
torque: 400,
|
||||||
|
topSpeed: 209,
|
||||||
|
acceleration0To100: 5.6,
|
||||||
|
fuelConsumption: 7.1,
|
||||||
|
dimensions: {
|
||||||
|
length: 4709,
|
||||||
|
width: 1827,
|
||||||
|
height: 1435,
|
||||||
|
wheelbase: 2851,
|
||||||
|
},
|
||||||
|
weight: 1620,
|
||||||
|
features: ['LED Headlights', 'iDrive Infotainment System', 'Leather Seats'],
|
||||||
|
images: ['https://example.com/bmw-330i-1.jpg', 'https://example.com/bmw-330i-2.jpg'],
|
||||||
|
modelId: findModelByName('3 Series')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'M340i Sedan',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Gasoline'],
|
||||||
|
horsePower: 382,
|
||||||
|
torque: 500,
|
||||||
|
topSpeed: 250,
|
||||||
|
acceleration0To100: 4.4,
|
||||||
|
fuelConsumption: 8.0,
|
||||||
|
dimensions: {
|
||||||
|
length: 4709,
|
||||||
|
width: 1827,
|
||||||
|
height: 1435,
|
||||||
|
wheelbase: 2851,
|
||||||
|
},
|
||||||
|
weight: 1670,
|
||||||
|
features: ['M Sport Differential', 'M Sport Brakes', 'Adaptive M Suspension'],
|
||||||
|
images: ['https://example.com/bmw-m340i-1.jpg', 'https://example.com/bmw-m340i-2.jpg'],
|
||||||
|
modelId: findModelByName('3 Series')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'X5 xDrive40i',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Gasoline'],
|
||||||
|
horsePower: 335,
|
||||||
|
torque: 450,
|
||||||
|
topSpeed: 243,
|
||||||
|
acceleration0To100: 5.5,
|
||||||
|
fuelConsumption: 9.2,
|
||||||
|
dimensions: {
|
||||||
|
length: 4922,
|
||||||
|
width: 2004,
|
||||||
|
height: 1745,
|
||||||
|
wheelbase: 2975,
|
||||||
|
},
|
||||||
|
weight: 2260,
|
||||||
|
features: ['Panoramic Roof', 'Head-Up Display', 'Gesture Control'],
|
||||||
|
images: ['https://example.com/bmw-x5-xdrive40i-1.jpg', 'https://example.com/bmw-x5-xdrive40i-2.jpg'],
|
||||||
|
modelId: findModelByName('X5')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Camry LE',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Gasoline'],
|
||||||
|
horsePower: 203,
|
||||||
|
torque: 250,
|
||||||
|
topSpeed: 210,
|
||||||
|
acceleration0To100: 8.1,
|
||||||
|
fuelConsumption: 7.6,
|
||||||
|
dimensions: {
|
||||||
|
length: 4880,
|
||||||
|
width: 1840,
|
||||||
|
height: 1445,
|
||||||
|
wheelbase: 2825,
|
||||||
|
},
|
||||||
|
weight: 1580,
|
||||||
|
features: ['Toyota Safety Sense', 'Apple CarPlay', 'Android Auto'],
|
||||||
|
images: ['https://example.com/toyota-camry-le-1.jpg', 'https://example.com/toyota-camry-le-2.jpg'],
|
||||||
|
modelId: findModelByName('Camry')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Camry Hybrid',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Hybrid'],
|
||||||
|
horsePower: 208,
|
||||||
|
torque: 220,
|
||||||
|
topSpeed: 180,
|
||||||
|
acceleration0To100: 7.8,
|
||||||
|
fuelConsumption: 4.2,
|
||||||
|
dimensions: {
|
||||||
|
length: 4880,
|
||||||
|
width: 1840,
|
||||||
|
height: 1445,
|
||||||
|
wheelbase: 2825,
|
||||||
|
},
|
||||||
|
weight: 1680,
|
||||||
|
features: ['Regenerative Braking', 'EV Mode', 'Energy Monitor'],
|
||||||
|
images: ['https://example.com/toyota-camry-hybrid-1.jpg', 'https://example.com/toyota-camry-hybrid-2.jpg'],
|
||||||
|
modelId: findModelByName('Camry')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Prius Prime',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Plug-in Hybrid'],
|
||||||
|
horsePower: 121,
|
||||||
|
torque: 142,
|
||||||
|
topSpeed: 165,
|
||||||
|
acceleration0To100: 10.5,
|
||||||
|
fuelConsumption: 1.8,
|
||||||
|
dimensions: {
|
||||||
|
length: 4645,
|
||||||
|
width: 1760,
|
||||||
|
height: 1470,
|
||||||
|
wheelbase: 2700,
|
||||||
|
},
|
||||||
|
weight: 1530,
|
||||||
|
features: ['Electric Range of 40 km', 'Touch-sensitive controls', 'Quad-LED projector headlights'],
|
||||||
|
images: ['https://example.com/toyota-prius-prime-1.jpg', 'https://example.com/toyota-prius-prime-2.jpg'],
|
||||||
|
modelId: findModelByName('Prius')?._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'A4 Prestige',
|
||||||
|
releaseYear: 2021,
|
||||||
|
engineTypes: ['Gasoline'],
|
||||||
|
horsePower: 261,
|
||||||
|
torque: 370,
|
||||||
|
topSpeed: 210,
|
||||||
|
acceleration0To100: 5.5,
|
||||||
|
fuelConsumption: 7.5,
|
||||||
|
dimensions: {
|
||||||
|
length: 4762,
|
||||||
|
width: 1847,
|
||||||
|
height: 1435,
|
||||||
|
wheelbase: 2820,
|
||||||
|
},
|
||||||
|
weight: 1640,
|
||||||
|
features: ['Audi Virtual Cockpit', 'Bang & Olufsen Sound System', 'Adaptive Cruise Control'],
|
||||||
|
images: ['https://example.com/audi-a4-prestige-1.jpg', 'https://example.com/audi-a4-prestige-2.jpg'],
|
||||||
|
modelId: findModelByName('A4')?._id,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all brands
|
* Get all brands
|
||||||
*/
|
*/
|
||||||
getAllBrands(): Promise<Brand[]> {
|
async getAllBrands(): Promise<Brand[]> {
|
||||||
return Promise.resolve(brands);
|
await this.connect();
|
||||||
|
if (!this.brandsCollection) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
return this.brandsCollection.find().toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get brand by ID
|
* Get brand by ID
|
||||||
*/
|
*/
|
||||||
getBrandById(id: string): Promise<Brand | null> {
|
async getBrandById(id: string): Promise<Brand | null> {
|
||||||
const brand = brands.find(brand => brand.id === id);
|
await this.connect();
|
||||||
return Promise.resolve(brand || null);
|
if (!this.brandsCollection) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
return this.brandsCollection.findOne({ _id: new ObjectId(id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all car models
|
* Get all car models
|
||||||
*/
|
*/
|
||||||
getAllCarModels(): Promise<CarModel[]> {
|
async getAllCarModels(): Promise<CarModel[]> {
|
||||||
return Promise.resolve(carModels);
|
await this.connect();
|
||||||
|
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
return this.carModelsCollection.find().toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get car models by brand ID
|
* Get car models by brand ID
|
||||||
*/
|
*/
|
||||||
getCarModelsByBrandId(brandId: string): Promise<CarModel[]> {
|
async getCarModelsByBrandId(brandId: string): Promise<CarModel[]> {
|
||||||
const brand = brands.find(brand => brand.id === brandId);
|
await this.connect();
|
||||||
return Promise.resolve(brand?.carModels || []);
|
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
return this.carModelsCollection.find({ brandId: new ObjectId(brandId) }).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get car model by ID
|
* Get car model by ID
|
||||||
*/
|
*/
|
||||||
getCarModelById(id: string): Promise<CarModel | null> {
|
async getCarModelById(id: string): Promise<CarModel | null> {
|
||||||
const carModel = carModels.find(model => model.id === id);
|
await this.connect();
|
||||||
return Promise.resolve(carModel || null);
|
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
return this.carModelsCollection.findOne({ _id: new ObjectId(id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all car revisions
|
* Get all car revisions
|
||||||
*/
|
*/
|
||||||
getAllCarRevisions(): Promise<CarRevision[]> {
|
async getAllCarRevisions(): Promise<CarRevision[]> {
|
||||||
return Promise.resolve(carRevisions);
|
await this.connect();
|
||||||
|
if (!this.carRevisionsCollection) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
return this.carRevisionsCollection.find().toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get car revisions by model ID
|
* Get car revisions by model ID
|
||||||
*/
|
*/
|
||||||
getCarRevisionsByModelId(modelId: string): Promise<CarRevision[]> {
|
async getCarRevisionsByModelId(modelId: string): Promise<CarRevision[]> {
|
||||||
const carModel = carModels.find(model => model.id === modelId);
|
await this.connect();
|
||||||
return Promise.resolve(carModel?.revisions || []);
|
if (!this.carRevisionsCollection) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
return this.carRevisionsCollection.find({ modelId: new ObjectId(modelId) }).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get car revision by ID
|
* Get car revision by ID
|
||||||
*/
|
*/
|
||||||
getCarRevisionById(id: string): Promise<CarRevision | null> {
|
async getCarRevisionById(id: string): Promise<CarRevision | null> {
|
||||||
const carRevision = carRevisions.find(revision => revision.id === id);
|
await this.connect();
|
||||||
return Promise.resolve(carRevision || null);
|
if (!this.carRevisionsCollection) throw new Error('Database not initialized');
|
||||||
|
|
||||||
|
return this.carRevisionsCollection.findOne({ _id: new ObjectId(id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search cars by name (searches both models and revisions)
|
* Search cars by name (searches both models and revisions)
|
||||||
*/
|
*/
|
||||||
searchCarsByName(name: string): Promise<{ models: CarModel[], revisions: CarRevision[] }> {
|
async searchCarsByName(name: string): Promise<{ models: CarModel[], revisions: CarRevision[], brands: Brand[] }> {
|
||||||
|
await this.connect();
|
||||||
|
if (!this.brandsCollection || !this.carModelsCollection || !this.carRevisionsCollection) {
|
||||||
|
throw new Error('Database not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
const lowercaseName = name.toLowerCase();
|
const lowercaseName = name.toLowerCase();
|
||||||
|
|
||||||
const matchingModels = carModels.filter(model =>
|
const matchingModels = await this.carModelsCollection.find({
|
||||||
model.name.toLowerCase().includes(lowercaseName)
|
name: { $regex: new RegExp(lowercaseName, 'i') }
|
||||||
);
|
}).toArray();
|
||||||
|
|
||||||
const matchingRevisions = carRevisions.filter(revision =>
|
const matchingRevisions = await this.carRevisionsCollection.find({
|
||||||
revision.name.toLowerCase().includes(lowercaseName)
|
name: { $regex: new RegExp(lowercaseName, 'i') }
|
||||||
);
|
}).toArray();
|
||||||
|
|
||||||
return Promise.resolve({
|
const matchingBrands = await this.brandsCollection.find({
|
||||||
|
name: { $regex: new RegExp(lowercaseName, 'i') }
|
||||||
|
}).toArray();
|
||||||
|
|
||||||
|
return {
|
||||||
models: matchingModels,
|
models: matchingModels,
|
||||||
revisions: matchingRevisions
|
revisions: matchingRevisions,
|
||||||
});
|
brands: matchingBrands
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cars by category
|
* Get cars by category
|
||||||
*/
|
*/
|
||||||
getCarsByCategory(category: string): Promise<CarModel[]> {
|
async getCarsByCategory(category: string): Promise<CarModel[]> {
|
||||||
const matchingModels = carModels.filter(model =>
|
await this.connect();
|
||||||
model.category?.toLowerCase() === category.toLowerCase()
|
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.resolve(matchingModels);
|
return this.carModelsCollection.find({
|
||||||
|
category: { $regex: new RegExp(`^${category}$`, 'i') }
|
||||||
|
}).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cars by year range
|
* Get cars by year range
|
||||||
*/
|
*/
|
||||||
getCarsByYearRange(startYear: number, endYear: number): Promise<CarModel[]> {
|
async getCarsByYearRange(startYear: number, endYear: number): Promise<CarModel[]> {
|
||||||
const matchingModels = carModels.filter(model => {
|
await this.connect();
|
||||||
const startYearMatch = !model.productionStartYear || model.productionStartYear <= endYear;
|
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||||
const endYearMatch = !model.productionEndYear || model.productionEndYear >= startYear;
|
|
||||||
return startYearMatch && endYearMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(matchingModels);
|
return this.carModelsCollection.find({
|
||||||
|
$or: [
|
||||||
|
{ productionStartYear: { $lte: endYear, $gte: startYear } },
|
||||||
|
{
|
||||||
|
productionStartYear: { $lte: endYear },
|
||||||
|
productionEndYear: { $gte: startYear }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
productionStartYear: { $lte: endYear },
|
||||||
|
productionEndYear: { $exists: false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).toArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user