diff --git a/.cursor/rules/instructions.mdc b/.cursor/rules/instructions.mdc index b81a943..1df5f17 100644 --- a/.cursor/rules/instructions.mdc +++ b/.cursor/rules/instructions.mdc @@ -12,4 +12,11 @@ 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 \ No newline at end of file +- No SCSS / SASS + +# Code Style + +This project follows clean code rules: +- SOLID Principles +- No else statements +- readable name \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..388e53a --- /dev/null +++ b/Dockerfile.dev @@ -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"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..57f5723 --- /dev/null +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3f407fe..a9c5157 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4a961dc..16f06e6 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/app/api/cars/brand/[id]/route.ts b/src/app/api/cars/brand/[id]/route.ts deleted file mode 100644 index 31b6bb0..0000000 --- a/src/app/api/cars/brand/[id]/route.ts +++ /dev/null @@ -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 } - ); - } -} \ No newline at end of file diff --git a/src/app/api/cars/brands/route.ts b/src/app/api/cars/brands/route.ts deleted file mode 100644 index d9ac35a..0000000 --- a/src/app/api/cars/brands/route.ts +++ /dev/null @@ -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 } - ); - } -} \ No newline at end of file diff --git a/src/app/api/cars/search/route.ts b/src/app/api/cars/search/route.ts index c3cdb75..2a4b258 100644 --- a/src/app/api/cars/search/route.ts +++ b/src/app/api/cars/search/route.ts @@ -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) { diff --git a/src/app/components/BrandList.tsx b/src/app/components/BrandList.tsx new file mode 100644 index 0000000..5e0233e --- /dev/null +++ b/src/app/components/BrandList.tsx @@ -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 ( +
+
+

Popular brands

+
+ { + e.currentTarget.style.backgroundColor = '#edae49'; + e.currentTarget.style.color = '#2e4057'; + }} + onMouseOut={(e) => { + e.currentTarget.style.backgroundColor = 'white'; + e.currentTarget.style.color = '#083d77'; + }} + >Tesla + { + e.currentTarget.style.backgroundColor = '#edae49'; + e.currentTarget.style.color = '#2e4057'; + }} + onMouseOut={(e) => { + e.currentTarget.style.backgroundColor = 'white'; + e.currentTarget.style.color = '#083d77'; + }} + >BMW + { + e.currentTarget.style.backgroundColor = '#edae49'; + e.currentTarget.style.color = '#2e4057'; + }} + onMouseOut={(e) => { + e.currentTarget.style.backgroundColor = 'white'; + e.currentTarget.style.color = '#083d77'; + }} + >Toyota + { + e.currentTarget.style.backgroundColor = '#edae49'; + e.currentTarget.style.color = '#2e4057'; + }} + onMouseOut={(e) => { + e.currentTarget.style.backgroundColor = 'white'; + e.currentTarget.style.color = '#083d77'; + }} + >Audi + { + e.currentTarget.style.backgroundColor = '#edae49'; + e.currentTarget.style.color = '#2e4057'; + }} + onMouseOut={(e) => { + e.currentTarget.style.backgroundColor = 'white'; + e.currentTarget.style.color = '#083d77'; + }} + >Mercedes +
+
+ ); +} \ No newline at end of file diff --git a/src/app/components/SearchBar.tsx b/src/app/components/SearchBar.tsx new file mode 100644 index 0000000..972b3b6 --- /dev/null +++ b/src/app/components/SearchBar.tsx @@ -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 ( +
+
+ 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'; + }} + /> + +
+
+ ); +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 9f65002..12829d6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 ( -
-

- E-WIKI -

- -
-
- setQuery(e.target.value)} - placeholder="Search for electric vehicles..." - className={styles.searchInput} - aria-label="Search for electric vehicles" - /> - -
-
- -
-

Popular brands

-
- Tesla - BMW - Toyota - Audi - Mercedes -
-
+
+

E-WIKI

+ +
); } diff --git a/src/app/results/page.tsx b/src/app/results/page.tsx index c27836a..8236f64 100644 --- a/src/app/results/page.tsx +++ b/src/app/results/page.tsx @@ -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([]); - const [revisions, setRevisions] = useState([]); + const [models, setModels] = useState([]); + const [revisions, setRevisions] = useState([]); const [error, setError] = useState(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 ( -
-
-
-

Search Results

- - New Search - -
+
+
+ {loading ? ( -
-
+
+
) : error ? ( -
+
{error}
) : models.length === 0 && revisions.length === 0 ? ( -
-

No results found

-

+

+

No results found

+

Try adjusting your search criteria to find more cars.

) : ( -
+
{models.length > 0 && ( -
-

Car Models

-
+
+

Car Models

+
{models.map((model) => (
-
+
{model.image ? ( {model.name} ) : ( -
+
No image available
)}
-
-
-

{model.name}

- +
+
+

{model.name}

+ {model.brand?.name}
-

{model.description}

-
- +

{model.description}

+
+ {model.category} - + Since {model.productionStartYear} {model.productionEndYear ? ` - ${model.productionEndYear}` @@ -135,63 +153,63 @@ export default function Results() { )} {revisions.length > 0 && ( -
-

Car Revisions

-
+
+

Car Revisions

+
{revisions.map((revision) => (
-
+
{revision.images && revision.images.length > 0 ? ( {revision.name} ) : ( -
+
No image available
)}
-
-
-

{revision.name}

- +
+
+

{revision.name}

+ {revision.baseModel?.brand?.name}
-
+
- Engine: + Engine: {revision.engineTypes?.join(', ')}
- Power: + Power: {revision.horsePower} HP
- 0-100 km/h: + 0-100 km/h: {revision.acceleration0To100}s
- Top Speed: + Top Speed: {revision.topSpeed} km/h
-
+
{revision.features?.slice(0, 3).map((feature, index) => ( {feature} ))} {revision.features && revision.features.length > 3 && ( - + +{revision.features.length - 3} more )} diff --git a/src/app/results/results.module.css b/src/app/results/results.module.css index f13d758..b1b13a4 100644 --- a/src/app/results/results.module.css +++ b/src/app/results/results.module.css @@ -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; } \ No newline at end of file diff --git a/src/backend/models/Brand.ts b/src/backend/models/Brand.ts index 6f1906c..ab80a9a 100644 --- a/src/backend/models/Brand.ts +++ b/src/backend/models/Brand.ts @@ -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; @@ -9,5 +11,4 @@ export interface Brand { headquarters: string; website: string; carModels?: CarModel[]; -} - +} \ No newline at end of file diff --git a/src/backend/models/CarModel.ts b/src/backend/models/CarModel.ts index fc014d4..533305c 100644 --- a/src/backend/models/CarModel.ts +++ b/src/backend/models/CarModel.ts @@ -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; } \ No newline at end of file diff --git a/src/backend/models/CarRevision.ts b/src/backend/models/CarRevision.ts index d50c7db..1e9fdfa 100644 --- a/src/backend/models/CarRevision.ts +++ b/src/backend/models/CarRevision.ts @@ -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; } \ No newline at end of file diff --git a/src/backend/repositories/CarRepository.ts b/src/backend/repositories/CarRepository.ts index 177de87..7d54dbc 100644 --- a/src/backend/repositories/CarRepository.ts +++ b/src/backend/repositories/CarRepository.ts @@ -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 | null = null; + private carModelsCollection: Collection | null = null; + private carRevisionsCollection: Collection | 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 { + if (!this.db) { + await this.client.connect(); + this.db = this.client.db(); + this.brandsCollection = this.db.collection('brands'); + this.carModelsCollection = this.db.collection('carModels'); + this.carRevisionsCollection = this.db.collection('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 { + 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 { + 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 { - return Promise.resolve(brands); + async getAllBrands(): Promise { + 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 { - const brand = brands.find(brand => brand.id === id); - return Promise.resolve(brand || null); + async getBrandById(id: string): Promise { + 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 { - return Promise.resolve(carModels); + async getAllCarModels(): Promise { + 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 { - const brand = brands.find(brand => brand.id === brandId); - return Promise.resolve(brand?.carModels || []); + async getCarModelsByBrandId(brandId: string): Promise { + 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 { - const carModel = carModels.find(model => model.id === id); - return Promise.resolve(carModel || null); + async getCarModelById(id: string): Promise { + 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 { - return Promise.resolve(carRevisions); + async getAllCarRevisions(): Promise { + 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 { - const carModel = carModels.find(model => model.id === modelId); - return Promise.resolve(carModel?.revisions || []); + async getCarRevisionsByModelId(modelId: string): Promise { + 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 { - const carRevision = carRevisions.find(revision => revision.id === id); - return Promise.resolve(carRevision || null); + async getCarRevisionById(id: string): Promise { + 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) - ); - - return Promise.resolve({ + const matchingRevisions = await this.carRevisionsCollection.find({ + name: { $regex: new RegExp(lowercaseName, 'i') } + }).toArray(); + + 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 { - const matchingModels = carModels.filter(model => - model.category?.toLowerCase() === category.toLowerCase() - ); + async getCarsByCategory(category: string): Promise { + 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 { - 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 { + 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(); } } \ No newline at end of file