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:
|
||||
- No Tailwind
|
||||
- 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",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"mongodb": "^6.16.0",
|
||||
"next": "15.3.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
@ -649,6 +650,15 @@
|
||||
"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": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz",
|
||||
@ -945,6 +955,21 @@
|
||||
"@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": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
||||
@ -1747,6 +1772,15 @@
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -3763,6 +3797,12 @@
|
||||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@ -3810,6 +3850,62 @@
|
||||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -4226,7 +4322,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -4683,6 +4778,15 @@
|
||||
"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": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||
@ -4941,6 +5045,18 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@ -5147,6 +5263,28 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"mongodb": "^6.16.0",
|
||||
"next": "15.3.2",
|
||||
"react": "^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) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
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 {
|
||||
let results;
|
||||
|
||||
// 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);
|
||||
}
|
||||
let results = await carRepository.searchCarsByName(query);
|
||||
|
||||
return NextResponse.json(results);
|
||||
} 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';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import styles from './home.module.css';
|
||||
import SearchBar from './components/SearchBar';
|
||||
import BrandList from './components/BrandList';
|
||||
|
||||
export default function Home() {
|
||||
const [query, setQuery] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (query.trim()) {
|
||||
router.push(`/results?query=${encodeURIComponent(query)}`);
|
||||
const styles = {
|
||||
container: {
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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 (
|
||||
<main className={styles.container}>
|
||||
<h1 className={styles.title}>
|
||||
<span className={styles.gradientText}>E-WIKI</span>
|
||||
</h1>
|
||||
|
||||
<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 style={styles.container}>
|
||||
<h1 style={styles.title}>E-WIKI</h1>
|
||||
<SearchBar />
|
||||
<BrandList />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,16 +2,33 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
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() {
|
||||
const searchParams = useSearchParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [models, setModels] = useState<CarModel[]>([]);
|
||||
const [revisions, setRevisions] = useState<CarRevision[]>([]);
|
||||
const [models, setModels] = useState<DisplayCarModel[]>([]);
|
||||
const [revisions, setRevisions] = useState<DisplayCarRevision[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const query = searchParams.get('query') || '';
|
||||
|
||||
useEffect(() => {
|
||||
const fetchResults = async () => {
|
||||
@ -56,71 +73,72 @@ export default function Results() {
|
||||
fetchResults();
|
||||
}, [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 (
|
||||
<main className="min-h-screen p-8 bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<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>
|
||||
<main className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<SearchBar initialQuery={query} />
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||
<div className={styles.loader}>
|
||||
<div className={styles.spinner}></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">
|
||||
<div className={styles.errorMessage}>
|
||||
{error}
|
||||
</div>
|
||||
) : models.length === 0 && revisions.length === 0 ? (
|
||||
<div className="bg-white p-8 rounded-lg shadow-md text-center">
|
||||
<h2 className="text-2xl font-semibold mb-4">No results found</h2>
|
||||
<p className="text-gray-600">
|
||||
<div className={styles.emptyResults}>
|
||||
<h2 className={styles.noResultsTitle}>No results found</h2>
|
||||
<p className={styles.noResultsText}>
|
||||
Try adjusting your search criteria to find more cars.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
{models.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">Car Models</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className={styles.sectionContent}>
|
||||
<h2 className={styles.sectionTitle}>Car Models</h2>
|
||||
<div className={styles.grid}>
|
||||
{models.map((model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="bg-white rounded-lg shadow-md overflow-hidden"
|
||||
key={getModelKey(model)}
|
||||
className={styles.card}
|
||||
>
|
||||
<div className="h-48 bg-gray-200 relative">
|
||||
<div className={styles.cardImage}>
|
||||
{model.image ? (
|
||||
<img
|
||||
src={model.image}
|
||||
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
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xl font-bold">{model.name}</h3>
|
||||
<span className="text-sm font-medium text-blue-600">
|
||||
<div className={styles.cardContent}>
|
||||
<div className={styles.cardHeader}>
|
||||
<h3 className={styles.cardTitle}>{model.name}</h3>
|
||||
<span className={styles.brandName}>
|
||||
{model.brand?.name}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mb-2">{model.description}</p>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="bg-gray-100 px-2 py-1 rounded">
|
||||
<p className={styles.cardDescription}>{model.description}</p>
|
||||
<div className={styles.cardFooter}>
|
||||
<span className={styles.category}>
|
||||
{model.category}
|
||||
</span>
|
||||
<span>
|
||||
<span className={styles.years}>
|
||||
Since {model.productionStartYear}
|
||||
{model.productionEndYear
|
||||
? ` - ${model.productionEndYear}`
|
||||
@ -135,63 +153,63 @@ export default function Results() {
|
||||
)}
|
||||
|
||||
{revisions.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">Car Revisions</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className={styles.sectionContent}>
|
||||
<h2 className={styles.sectionTitle}>Car Revisions</h2>
|
||||
<div className={styles.grid}>
|
||||
{revisions.map((revision) => (
|
||||
<div
|
||||
key={revision.id}
|
||||
className="bg-white rounded-lg shadow-md overflow-hidden"
|
||||
key={getRevisionKey(revision)}
|
||||
className={styles.card}
|
||||
>
|
||||
<div className="h-48 bg-gray-200 relative">
|
||||
<div className={styles.cardImage}>
|
||||
{revision.images && revision.images.length > 0 ? (
|
||||
<img
|
||||
src={revision.images[0]}
|
||||
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
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xl font-bold">{revision.name}</h3>
|
||||
<span className="text-sm font-medium text-blue-600">
|
||||
<div className={styles.cardContent}>
|
||||
<div className={styles.cardHeader}>
|
||||
<h3 className={styles.cardTitle}>{revision.name}</h3>
|
||||
<span className={styles.brandName}>
|
||||
{revision.baseModel?.brand?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 mb-2 text-sm">
|
||||
<div className={styles.specGrid}>
|
||||
<div>
|
||||
<span className="font-medium">Engine: </span>
|
||||
<span className={styles.specLabel}>Engine: </span>
|
||||
{revision.engineTypes?.join(', ')}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Power: </span>
|
||||
<span className={styles.specLabel}>Power: </span>
|
||||
{revision.horsePower} HP
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">0-100 km/h: </span>
|
||||
<span className={styles.specLabel}>0-100 km/h: </span>
|
||||
{revision.acceleration0To100}s
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Top Speed: </span>
|
||||
<span className={styles.specLabel}>Top Speed: </span>
|
||||
{revision.topSpeed} km/h
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
<div className={styles.tagContainer}>
|
||||
{revision.features?.slice(0, 3).map((feature, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded"
|
||||
key={`${getRevisionKey(revision)}-feature-${index}`}
|
||||
className={styles.tag}
|
||||
>
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
{revision.features && revision.features.length > 3 && (
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className={styles.tagCount}>
|
||||
+{revision.features.length - 3} more
|
||||
</span>
|
||||
)}
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
.resultsContainer {
|
||||
/* Results page specific styles */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, var(--background), rgba(8, 61, 119, 0.05));
|
||||
padding: 2rem;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
max-width: 80rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -14,26 +10,38 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
.resultsTitle {
|
||||
font-size: 1.875rem;
|
||||
font-weight: bold;
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
|
||||
.backButton {
|
||||
background-color: #3b82f6;
|
||||
background-color: var(--color-yale-blue);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: background-color 0.2s;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 25px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backButton:hover {
|
||||
background-color: #2563eb;
|
||||
background-color: var(--color-charcoal);
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
.content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -41,11 +49,11 @@
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border-radius: 50%;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 0.25rem solid rgba(59, 130, 246, 0.1);
|
||||
border-top-color: #3b82f6;
|
||||
border: 0.25rem solid rgba(8, 61, 119, 0.1);
|
||||
border-top: 0.25rem solid var(--color-yale-blue);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@ -54,56 +62,64 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.errorContainer {
|
||||
background-color: #fee2e2;
|
||||
border: 1px solid #f87171;
|
||||
color: #b91c1c;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.emptyResultsContainer {
|
||||
background-color: white;
|
||||
padding: 2rem;
|
||||
.errorMessage {
|
||||
background-color: #fde8e8;
|
||||
border: 1px solid #f8b4b4;
|
||||
color: #9b1c1c;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
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-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
|
||||
.emptyResultsMessage {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.resultsSection {
|
||||
margin-bottom: 2rem;
|
||||
.noResultsText {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
|
||||
.cardsGrid {
|
||||
.sectionContent {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.cardsGrid {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.cardsGrid {
|
||||
.grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
@ -111,23 +127,29 @@
|
||||
.card {
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.cardImageContainer {
|
||||
height: 12rem;
|
||||
background-color: #e5e7eb;
|
||||
position: relative;
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cardImage {
|
||||
height: 12rem;
|
||||
background-color: #f3f4f6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.noImageContainer {
|
||||
.noImage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -148,17 +170,18 @@
|
||||
|
||||
.cardTitle {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
font-weight: 700;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
|
||||
.brandName {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #3b82f6;
|
||||
color: var(--color-yale-blue);
|
||||
}
|
||||
|
||||
.cardDescription {
|
||||
color: #6b7280;
|
||||
color: #4b5563;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@ -174,9 +197,14 @@
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.specsGrid {
|
||||
.years {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.specGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
@ -186,16 +214,17 @@
|
||||
|
||||
.specLabel {
|
||||
font-weight: 500;
|
||||
color: var(--color-charcoal);
|
||||
}
|
||||
|
||||
.featuresList {
|
||||
.tagContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.featureTag {
|
||||
.tag {
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
font-size: 0.75rem;
|
||||
@ -203,7 +232,7 @@
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.moreFeatures {
|
||||
.tagCount {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
import { CarModel } from './CarModel';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
export interface Brand {
|
||||
id: string;
|
||||
_id?: ObjectId;
|
||||
id?: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
@ -10,4 +12,3 @@ export interface Brand {
|
||||
website: string;
|
||||
carModels?: CarModel[];
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { Brand } from './Brand';
|
||||
import { CarRevision } from './CarRevision';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
export interface CarModel {
|
||||
id: string;
|
||||
_id?: ObjectId;
|
||||
id?: string;
|
||||
name: string;
|
||||
brand: Brand;
|
||||
productionStartYear: number;
|
||||
productionEndYear?: number;
|
||||
category?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
revisions?: CarRevision[];
|
||||
brandId?: ObjectId;
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { CarModel } from './CarModel';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
export interface Dimensions {
|
||||
length: number;
|
||||
@ -8,9 +9,9 @@ export interface Dimensions {
|
||||
}
|
||||
|
||||
export interface CarRevision {
|
||||
id: string;
|
||||
_id?: ObjectId;
|
||||
id?: string;
|
||||
name: string;
|
||||
baseModel: CarModel;
|
||||
releaseYear: number;
|
||||
engineTypes: string[];
|
||||
horsePower: number;
|
||||
@ -22,4 +23,5 @@ export interface CarRevision {
|
||||
weight: number;
|
||||
features: string[];
|
||||
images: string[];
|
||||
modelId?: ObjectId;
|
||||
}
|
||||
@ -1,512 +1,540 @@
|
||||
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 {
|
||||
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
|
||||
*/
|
||||
getAllBrands(): Promise<Brand[]> {
|
||||
return Promise.resolve(brands);
|
||||
async getAllBrands(): Promise<Brand[]> {
|
||||
await this.connect();
|
||||
if (!this.brandsCollection) throw new Error('Database not initialized');
|
||||
|
||||
return this.brandsCollection.find().toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get brand by ID
|
||||
*/
|
||||
getBrandById(id: string): Promise<Brand | null> {
|
||||
const brand = brands.find(brand => brand.id === id);
|
||||
return Promise.resolve(brand || null);
|
||||
async getBrandById(id: string): Promise<Brand | null> {
|
||||
await this.connect();
|
||||
if (!this.brandsCollection) throw new Error('Database not initialized');
|
||||
|
||||
return this.brandsCollection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all car models
|
||||
*/
|
||||
getAllCarModels(): Promise<CarModel[]> {
|
||||
return Promise.resolve(carModels);
|
||||
async getAllCarModels(): Promise<CarModel[]> {
|
||||
await this.connect();
|
||||
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||
|
||||
return this.carModelsCollection.find().toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get car models by brand ID
|
||||
*/
|
||||
getCarModelsByBrandId(brandId: string): Promise<CarModel[]> {
|
||||
const brand = brands.find(brand => brand.id === brandId);
|
||||
return Promise.resolve(brand?.carModels || []);
|
||||
async getCarModelsByBrandId(brandId: string): Promise<CarModel[]> {
|
||||
await this.connect();
|
||||
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||
|
||||
return this.carModelsCollection.find({ brandId: new ObjectId(brandId) }).toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get car model by ID
|
||||
*/
|
||||
getCarModelById(id: string): Promise<CarModel | null> {
|
||||
const carModel = carModels.find(model => model.id === id);
|
||||
return Promise.resolve(carModel || null);
|
||||
async getCarModelById(id: string): Promise<CarModel | null> {
|
||||
await this.connect();
|
||||
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||
|
||||
return this.carModelsCollection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all car revisions
|
||||
*/
|
||||
getAllCarRevisions(): Promise<CarRevision[]> {
|
||||
return Promise.resolve(carRevisions);
|
||||
async getAllCarRevisions(): Promise<CarRevision[]> {
|
||||
await this.connect();
|
||||
if (!this.carRevisionsCollection) throw new Error('Database not initialized');
|
||||
|
||||
return this.carRevisionsCollection.find().toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get car revisions by model ID
|
||||
*/
|
||||
getCarRevisionsByModelId(modelId: string): Promise<CarRevision[]> {
|
||||
const carModel = carModels.find(model => model.id === modelId);
|
||||
return Promise.resolve(carModel?.revisions || []);
|
||||
async getCarRevisionsByModelId(modelId: string): Promise<CarRevision[]> {
|
||||
await this.connect();
|
||||
if (!this.carRevisionsCollection) throw new Error('Database not initialized');
|
||||
|
||||
return this.carRevisionsCollection.find({ modelId: new ObjectId(modelId) }).toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get car revision by ID
|
||||
*/
|
||||
getCarRevisionById(id: string): Promise<CarRevision | null> {
|
||||
const carRevision = carRevisions.find(revision => revision.id === id);
|
||||
return Promise.resolve(carRevision || null);
|
||||
async getCarRevisionById(id: string): Promise<CarRevision | null> {
|
||||
await this.connect();
|
||||
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)
|
||||
*/
|
||||
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 matchingModels = carModels.filter(model =>
|
||||
model.name.toLowerCase().includes(lowercaseName)
|
||||
);
|
||||
const matchingModels = await this.carModelsCollection.find({
|
||||
name: { $regex: new RegExp(lowercaseName, 'i') }
|
||||
}).toArray();
|
||||
|
||||
const matchingRevisions = carRevisions.filter(revision =>
|
||||
revision.name.toLowerCase().includes(lowercaseName)
|
||||
);
|
||||
const matchingRevisions = await this.carRevisionsCollection.find({
|
||||
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,
|
||||
revisions: matchingRevisions
|
||||
});
|
||||
revisions: matchingRevisions,
|
||||
brands: matchingBrands
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cars by category
|
||||
*/
|
||||
getCarsByCategory(category: string): Promise<CarModel[]> {
|
||||
const matchingModels = carModels.filter(model =>
|
||||
model.category?.toLowerCase() === category.toLowerCase()
|
||||
);
|
||||
async getCarsByCategory(category: string): Promise<CarModel[]> {
|
||||
await this.connect();
|
||||
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
|
||||
*/
|
||||
getCarsByYearRange(startYear: number, endYear: number): Promise<CarModel[]> {
|
||||
const matchingModels = carModels.filter(model => {
|
||||
const startYearMatch = !model.productionStartYear || model.productionStartYear <= endYear;
|
||||
const endYearMatch = !model.productionEndYear || model.productionEndYear >= startYear;
|
||||
return startYearMatch && endYearMatch;
|
||||
});
|
||||
async getCarsByYearRange(startYear: number, endYear: number): Promise<CarModel[]> {
|
||||
await this.connect();
|
||||
if (!this.carModelsCollection) throw new Error('Database not initialized');
|
||||
|
||||
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