From d028c67c68a495ad99366614a38953b6c156c421 Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Mon, 19 May 2025 08:42:49 +0200 Subject: [PATCH] inital commit --- .cursor/rules/instructions.mdc | 15 + .cursor/rules/project.mdc | 14 + package-lock.json | 10 + package.json | 9 +- src/app/api/cars/brand/[id]/route.ts | 22 + src/app/api/cars/brands/route.ts | 17 + src/app/api/cars/search/route.ts | 39 ++ src/app/components.css | 201 +++++++++ src/app/globals.css | 26 +- src/app/home.module.css | 180 ++++++++ src/app/layout.tsx | 5 +- src/app/page.module.css | 303 +++++++++++++ src/app/page.tsx | 140 +++--- src/app/results/page.tsx | 210 +++++++++ src/app/results/results.module.css | 209 +++++++++ src/app/services/carService.ts | 80 ++++ src/backend/models/Brand.ts | 13 + src/backend/models/CarModel.ts | 14 + src/backend/models/CarRevision.ts | 25 ++ src/backend/models/index.ts | 3 + src/backend/repositories/CarRepository.ts | 512 ++++++++++++++++++++++ src/backend/repositories/index.ts | 1 + 22 files changed, 1944 insertions(+), 104 deletions(-) create mode 100644 .cursor/rules/instructions.mdc create mode 100644 .cursor/rules/project.mdc create mode 100644 src/app/api/cars/brand/[id]/route.ts create mode 100644 src/app/api/cars/brands/route.ts create mode 100644 src/app/api/cars/search/route.ts create mode 100644 src/app/components.css create mode 100644 src/app/home.module.css create mode 100644 src/app/results/page.tsx create mode 100644 src/app/results/results.module.css create mode 100644 src/app/services/carService.ts create mode 100644 src/backend/models/Brand.ts create mode 100644 src/backend/models/CarModel.ts create mode 100644 src/backend/models/CarRevision.ts create mode 100644 src/backend/models/index.ts create mode 100644 src/backend/repositories/CarRepository.ts create mode 100644 src/backend/repositories/index.ts diff --git a/.cursor/rules/instructions.mdc b/.cursor/rules/instructions.mdc new file mode 100644 index 0000000..b81a943 --- /dev/null +++ b/.cursor/rules/instructions.mdc @@ -0,0 +1,15 @@ +--- +description: +globs: +alwaysApply: true +--- +# Description + +This a fullstack nextjs project, all written in mordern and state of the art typescript. +The Project follows the latest coding standards for next js projects + +# Modules + +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 diff --git a/.cursor/rules/project.mdc b/.cursor/rules/project.mdc new file mode 100644 index 0000000..b356ec6 --- /dev/null +++ b/.cursor/rules/project.mdc @@ -0,0 +1,14 @@ +--- +description: +globs: +alwaysApply: true +--- +# EWIKI + +The EWIKI is a eletric vehicle database, that allows the user to search and compare any information about eletric vehicles. + +# Structure + +## Start page + +The start page looks like a modern web search engine. There are no fancy input and filter options, just a regular search bar \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f76ade0..3f407fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "evwiki", "version": "0.1.0", "dependencies": { + "@heroicons/react": "^2.2.0", "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -196,6 +197,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/package.json b/package.json index 75556b8..4a961dc 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,18 @@ "lint": "next lint" }, "dependencies": { + "@heroicons/react": "^2.2.0", + "next": "15.3.2", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.2" + "react-dom": "^19.0.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.2", - "@eslint/eslintrc": "^3" + "typescript": "^5" } } diff --git a/src/app/api/cars/brand/[id]/route.ts b/src/app/api/cars/brand/[id]/route.ts new file mode 100644 index 0000000..31b6bb0 --- /dev/null +++ b/src/app/api/cars/brand/[id]/route.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..d9ac35a --- /dev/null +++ b/src/app/api/cars/brands/route.ts @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..c3cdb75 --- /dev/null +++ b/src/app/api/cars/search/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CarRepository } from '../../../../backend/repositories/CarRepository'; + +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); + } + + return NextResponse.json(results); + } catch (error) { + console.error('Search error:', error); + return NextResponse.json( + { error: 'Failed to search cars' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/components.css b/src/app/components.css new file mode 100644 index 0000000..7f620e8 --- /dev/null +++ b/src/app/components.css @@ -0,0 +1,201 @@ +/* Components CSS - Modern simple design with rounded corners */ + +/* Buttons */ +.btn { + display: inline-block; + padding: 0.6rem 1.2rem; + font-size: 1rem; + font-weight: 500; + text-align: center; + border: none; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease-in-out; +} + +.btn-primary { + background-color: var(--color-yale-blue); + color: white; +} + +.btn-primary:hover { + background-color: #0a4e96; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.btn-secondary { + background-color: var(--color-cerise); + color: white; +} + +.btn-secondary:hover { + background-color: #e4547a; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.btn-outline { + background-color: transparent; + color: var(--color-yale-blue); + border: 1px solid var(--color-yale-blue); +} + +.btn-outline:hover { + background-color: rgba(8, 61, 119, 0.05); +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + margin-bottom: 0.5rem; + font-weight: 600; + line-height: 1.2; + color: var(--foreground); +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +p { + margin-bottom: 1rem; + line-height: 1.5; +} + +/* Links */ +.link { + color: var(--color-yale-blue); + text-decoration: none; + transition: color 0.2s; + border-bottom: 1px solid transparent; +} + +.link:hover { + color: var(--color-cerise); + border-bottom: 1px solid var(--color-cerise); +} + +/* Form Elements */ +.form-group { + margin-bottom: 1rem; +} + +.form-label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.form-input { + display: block; + width: 100%; + padding: 0.6rem 0.8rem; + font-size: 1rem; + line-height: 1.5; + color: var(--foreground); + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.5rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-input:focus { + color: var(--foreground); + background-color: #fff; + border-color: var(--color-yale-blue); + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(8, 61, 119, 0.25); +} + +.form-select { + display: block; + width: 100%; + padding: 0.6rem 2rem 0.6rem 0.8rem; + font-size: 1rem; + line-height: 1.5; + color: var(--foreground); + background-color: #fff; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23343a40' d='M6 8.5l4-4 1 1-5 5-5-5 1-1z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.8rem center; + background-size: 12px 12px; + border: 1px solid #ced4da; + border-radius: 0.5rem; + appearance: none; +} + +.form-checkbox, .form-radio { + display: inline-block; + margin-right: 0.5rem; + cursor: pointer; +} + +/* Card */ +.card { + background-color: white; + border-radius: 0.75rem; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.card-header { + margin-bottom: 1rem; + border-bottom: 1px solid #efefef; + padding-bottom: 0.75rem; +} + +.card-title { + margin-bottom: 0.25rem; +} + +.card-body { + padding: 0.5rem 0; +} + +/* Badge */ +.badge { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.375rem; +} + +.badge-primary { + background-color: var(--color-yale-blue); + color: white; +} + +.badge-secondary { + background-color: var(--color-cerise); + color: white; +} + +.badge-light { + background-color: var(--color-naples-yellow); + color: var(--color-charcoal); +} \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index e3734be..d44bd94 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,13 +1,16 @@ :root { - --background: #ffffff; - --foreground: #171717; -} + --color-sunset: #F6D8AE; + --color-charcoal: #2E4057; + --color-yale-blue: #083D77; + --color-cerise: #DA4167; + --color-naples-yellow: #F4D35E; -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + /* Neutral background colors */ + --color-light-neutral: #F5F5F5; + --color-dark-neutral: #1F1F1F; + + --background: var(--color-light-neutral); + --foreground: var(--color-charcoal); } html, @@ -17,8 +20,8 @@ body { } body { - color: var(--foreground); background: var(--background); + color: var(--foreground); font-family: Arial, Helvetica, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -40,3 +43,8 @@ a { color-scheme: dark; } } + +.button-primary { + background: var(--color-yale-blue); + color: #fff; +} diff --git a/src/app/home.module.css b/src/app/home.module.css new file mode 100644 index 0000000..24eb7df --- /dev/null +++ b/src/app/home.module.css @@ -0,0 +1,180 @@ +/* Home page specific styles */ +.container { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + background: linear-gradient(135deg, var(--background), rgba(8, 61, 119, 0.05)); +} + +.title { + font-size: 3.5rem; + font-weight: 800; + margin-bottom: 2.5rem; + text-align: center; + letter-spacing: -1px; + animation: fadeIn 0.8s ease-in-out; +} + +.gradientText { + background: linear-gradient(90deg, + var(--color-yale-blue) 0%, + var(--color-charcoal) 35%, + var(--color-cerise) 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + display: inline-block; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-20px); } + to { opacity: 1; transform: translateY(0); } +} + +.searchContainer { + width: 100%; + max-width: 42rem; + margin-bottom: 2.5rem; + animation: scaleIn 0.5s ease-in-out; + animation-delay: 0.3s; + animation-fill-mode: both; +} + +@keyframes scaleIn { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +.searchForm { + display: flex; + width: 100%; + position: relative; +} + +.searchInput { + width: 100%; + padding: 1.2rem 1.5rem; + font-size: 1.2rem; + border: 2px solid transparent; + border-radius: 30px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + outline: none; + transition: all 0.3s ease; + background-color: white; + color: var(--color-charcoal); +} + +.searchInput:focus { + box-shadow: 0 6px 16px rgba(8, 61, 119, 0.2); + border-color: var(--color-yale-blue); +} + +.searchInput::placeholder { + color: rgba(46, 64, 87, 0.5); +} + +.searchButton { + position: absolute; + right: 5px; + top: 5px; + bottom: 5px; + padding: 0 1.8rem; + background-color: var(--color-yale-blue); + color: white; + border: none; + border-radius: 25px; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.searchButton:hover { + background-color: var(--color-charcoal); +} + +.card { + width: 100%; + max-width: 42rem; + background-color: white; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.subtitle { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1.5rem; + color: var(--foreground); +} + +.form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.yearGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.brandSection { + margin-top: 2rem; + text-align: center; + color: var(--color-charcoal); + animation: fadeIn 0.5s ease-in-out; + animation-delay: 0.6s; + animation-fill-mode: both; + position: relative; +} + +.brandSection::before { + content: ''; + position: absolute; + top: -15px; + left: 50%; + transform: translateX(-50%); + width: 50px; + height: 3px; + background-color: var(--color-naples-yellow); + border-radius: 3px; +} + +.brandSection p { + font-weight: 600; + margin-bottom: 0.8rem; + font-size: 1.1rem; +} + +.brandList { + margin-top: 0.8rem; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 1.2rem; +} + +.brandLink { + color: var(--color-yale-blue); + text-decoration: none; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 20px; + transition: all 0.2s ease; + background-color: white; + border: 1px solid rgba(8, 61, 119, 0.1); +} + +.brandLink:hover { + background-color: var(--color-sunset); + color: var(--color-charcoal); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 42fc323..adb04af 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import "./components.css"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "EV WIKI", + description: "Modern search engine for electric vehicle information", }; export default function RootLayout({ diff --git a/src/app/page.module.css b/src/app/page.module.css index a11c8f3..0670dbb 100644 --- a/src/app/page.module.css +++ b/src/app/page.module.css @@ -166,3 +166,306 @@ a.secondary { filter: invert(); } } + +.searchPage { + display: flex; + flex-direction: column; + min-height: 100vh; + align-items: center; + justify-content: space-between; + font-family: var(--font-geist-sans); + background: var(--background); + color: var(--foreground); +} + +.searchMain { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + width: 100%; + max-width: 800px; + padding: 0 24px; +} + +.logoContainer { + margin-bottom: 36px; + text-align: center; +} + +.logo { + font-size: 4rem; + font-weight: 700; + letter-spacing: -1px; + background: linear-gradient(to right, var(--color-yale-blue), var(--color-cerise)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + margin: 0; +} + +.searchContainer { + width: 100%; + max-width: 600px; + margin-bottom: 24px; +} + +.searchBox { + display: flex; + width: 100%; + position: relative; + border-radius: 24px; + background: #fff; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: box-shadow 0.3s ease; +} + +.searchBox:hover, .searchBox:focus-within { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.searchInput { + flex: 1; + height: 48px; + padding: 0 16px; + font-size: 16px; + border: none; + outline: none; + background: transparent; + color: #333; +} + +.searchButton { + display: flex; + align-items: center; + justify-content: center; + padding: 0 16px; + background: none; + border: none; + cursor: pointer; + color: var(--color-yale-blue); + transition: color 0.3s ease; +} + +.searchButton:hover { + color: var(--color-cerise); +} + +.searchIcon { + width: 24px; + height: 24px; +} + +.suggestionsContainer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + margin-top: 12px; +} + +.suggestionChip { + padding: 8px 16px; + background: rgba(8, 61, 119, 0.08); + border: 1px solid rgba(8, 61, 119, 0.15); + border-radius: 20px; + font-size: 14px; + font-weight: 500; + color: var(--foreground); + cursor: pointer; + transition: all 0.2s ease; +} + +.suggestionChip:hover { + background: var(--color-yale-blue); + color: white; + border-color: var(--color-yale-blue); +} + +.searchFooter { + width: 100%; + padding: 16px 0; + border-top: 1px solid rgba(var(--gray-rgb), 0.1); + font-size: 14px; +} + +.footerLinks { + display: flex; + justify-content: center; + gap: 24px; +} + +.footerLinks a { + color: var(--foreground); + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.footerLinks a:hover { + opacity: 1; + text-decoration: underline; +} + +@media (max-width: 600px) { + .logo { + font-size: 3rem; + } + + .searchBox { + border-radius: 20px; + } + + .searchInput { + height: 44px; + } + + .suggestionsContainer { + flex-direction: column; + align-items: center; + } + + .suggestionChip { + width: 100%; + max-width: 280px; + text-align: center; + } + + .footerLinks { + flex-wrap: wrap; + gap: 16px; + justify-content: center; + } +} + +@media (prefers-color-scheme: dark) { + .searchBox { + background: rgba(255, 255, 255, 0.05); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + } + + .searchBox:hover, .searchBox:focus-within { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + .searchInput { + color: var(--foreground); + } + + .searchButton { + color: var(--color-yale-blue); + } + + .searchButton:hover { + color: var(--color-cerise); + } + + .suggestionChip { + background: rgba(255, 255, 255, 0.07); + border: 1px solid rgba(255, 255, 255, 0.15); + } +} + +.pageContainer { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + background-color: #f9fafb; +} + +.mainTitle { + font-size: 2.5rem; + font-weight: bold; + margin-bottom: 2rem; +} + +.searchCard { + width: 100%; + max-width: 40rem; + background-color: white; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.cardTitle { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1.5rem; +} + +.formGroup { + margin-bottom: 1rem; +} + +.label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #4b5563; + margin-bottom: 0.25rem; +} + +.input, +.select { + width: 100%; + padding: 0.5rem 1rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + font-size: 1rem; +} + +.input:focus, +.select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.yearGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.button { + width: 100%; + background-color: #3b82f6; + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 0.375rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.button:hover { + background-color: #2563eb; +} + +.brandSection { + margin-top: 1.5rem; + text-align: center; + color: #6b7280; +} + +.brandList { + margin-top: 0.5rem; + display: flex; + justify-content: center; + gap: 1rem; +} + +.brandLink { + color: #3b82f6; +} + +.brandLink:hover { + text-decoration: underline; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 84af2cb..9f65002 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,95 +1,57 @@ -import Image from "next/image"; -import styles from "./page.module.css"; +'use client'; + +import { useState, FormEvent } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import styles from './home.module.css'; export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing src/app/page.tsx. -
  2. -
  3. Save and see your changes instantly.
  4. -
+ const [query, setQuery] = useState(''); + const router = useRouter(); -
- - Vercel logomark - Deploy now - - - Read our docs - + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + if (query.trim()) { + router.push(`/results?query=${encodeURIComponent(query)}`); + } + }; + + 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
-
- -
+
+ ); } diff --git a/src/app/results/page.tsx b/src/app/results/page.tsx new file mode 100644 index 0000000..c27836a --- /dev/null +++ b/src/app/results/page.tsx @@ -0,0 +1,210 @@ +'use client'; + +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'; + +export default function Results() { + const searchParams = useSearchParams(); + const [loading, setLoading] = useState(true); + const [models, setModels] = useState([]); + const [revisions, setRevisions] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchResults = async () => { + try { + setLoading(true); + setError(null); + + const query = searchParams.get('query') || ''; + const category = searchParams.get('category') || ''; + const startYear = searchParams.get('startYear'); + const endYear = searchParams.get('endYear'); + const brandId = searchParams.get('brand'); + + let results; + + if (brandId) { + // Handle brand-specific search (if implemented in CarService) + const response = await fetch(`/api/cars/brand/${brandId}`); + if (!response.ok) throw new Error('Failed to fetch brand models'); + results = await response.json(); + } else if (category) { + results = await CarService.searchByCategory(category); + } else if (startYear && endYear) { + results = await CarService.searchByYearRange( + parseInt(startYear), + parseInt(endYear) + ); + } else { + results = await CarService.searchByQuery(query); + } + + setModels(results.models || []); + setRevisions(results.revisions || []); + } catch (err) { + console.error('Error fetching search results:', err); + setError('Failed to load search results. Please try again.'); + } finally { + setLoading(false); + } + }; + + fetchResults(); + }, [searchParams]); + + return ( +
+
+
+

Search Results

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

No results found

+

+ Try adjusting your search criteria to find more cars. +

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

Car Models

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

{model.name}

+ + {model.brand?.name} + +
+

{model.description}

+
+ + {model.category} + + + Since {model.productionStartYear} + {model.productionEndYear + ? ` - ${model.productionEndYear}` + : ''} + +
+
+
+ ))} +
+
+ )} + + {revisions.length > 0 && ( +
+

Car Revisions

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

{revision.name}

+ + {revision.baseModel?.brand?.name} + +
+
+
+ Engine: + {revision.engineTypes?.join(', ')} +
+
+ Power: + {revision.horsePower} HP +
+
+ 0-100 km/h: + {revision.acceleration0To100}s +
+
+ 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 + + )} +
+
+
+ ))} +
+
+ )} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/results/results.module.css b/src/app/results/results.module.css new file mode 100644 index 0000000..f13d758 --- /dev/null +++ b/src/app/results/results.module.css @@ -0,0 +1,209 @@ +.resultsContainer { + min-height: 100vh; + padding: 2rem; + background-color: #f9fafb; +} + +.contentContainer { + max-width: 80rem; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.resultsTitle { + font-size: 1.875rem; + font-weight: bold; +} + +.backButton { + background-color: #3b82f6; + color: white; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + transition: background-color 0.2s; +} + +.backButton:hover { + background-color: #2563eb; +} + +.loadingContainer { + display: flex; + justify-content: center; + align-items: center; + padding: 3rem 0; +} + +.spinner { + border-radius: 50%; + width: 3rem; + height: 3rem; + border: 0.25rem solid rgba(59, 130, 246, 0.1); + border-top-color: #3b82f6; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 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; + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.emptyResultsTitle { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.emptyResultsMessage { + color: #6b7280; +} + +.resultsSection { + margin-bottom: 2rem; +} + +.sectionTitle { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.cardsGrid { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: 1.5rem; +} + +@media (min-width: 768px) { + .cardsGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .cardsGrid { + grid-template-columns: repeat(3, 1fr); + } +} + +.card { + background-color: white; + border-radius: 0.5rem; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.cardImageContainer { + height: 12rem; + background-color: #e5e7eb; + position: relative; +} + +.cardImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.noImageContainer { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #6b7280; +} + +.cardContent { + padding: 1rem; +} + +.cardHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.cardTitle { + font-size: 1.25rem; + font-weight: bold; +} + +.brandName { + font-size: 0.875rem; + font-weight: 500; + color: #3b82f6; +} + +.cardDescription { + color: #6b7280; + font-size: 0.875rem; + margin-bottom: 0.5rem; +} + +.cardFooter { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; +} + +.category { + background-color: #f3f4f6; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +.specsGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.specLabel { + font-weight: 500; +} + +.featuresList { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + margin-top: 0.5rem; +} + +.featureTag { + background-color: #dbeafe; + color: #1e40af; + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +.moreFeatures { + font-size: 0.75rem; + color: #6b7280; +} \ No newline at end of file diff --git a/src/app/services/carService.ts b/src/app/services/carService.ts new file mode 100644 index 0000000..49df3d3 --- /dev/null +++ b/src/app/services/carService.ts @@ -0,0 +1,80 @@ +import { Brand, CarModel, CarRevision } from '../../backend/models'; + +interface SearchResults { + models: CarModel[]; + revisions: CarRevision[]; +} + +export class CarService { + /** + * Search cars by free-text query + */ + static async searchByQuery(query: string): Promise { + try { + const response = await fetch(`/api/cars/search?query=${encodeURIComponent(query)}`); + + if (!response.ok) { + throw new Error('Failed to search cars'); + } + + return await response.json(); + } catch (error) { + console.error('Error searching cars:', error); + return { models: [], revisions: [] }; + } + } + + /** + * Search cars by category + */ + static async searchByCategory(category: string): Promise { + try { + const response = await fetch(`/api/cars/search?category=${encodeURIComponent(category)}`); + + if (!response.ok) { + throw new Error('Failed to search cars by category'); + } + + return await response.json(); + } catch (error) { + console.error('Error searching cars by category:', error); + return { models: [], revisions: [] }; + } + } + + /** + * Search cars by year range + */ + static async searchByYearRange(startYear: number, endYear: number): Promise { + try { + const response = await fetch(`/api/cars/search?startYear=${startYear}&endYear=${endYear}`); + + if (!response.ok) { + throw new Error('Failed to search cars by year range'); + } + + return await response.json(); + } catch (error) { + console.error('Error searching cars by year range:', error); + return { models: [], revisions: [] }; + } + } + + /** + * Get all car brands + */ + static async getAllBrands(): Promise { + try { + const response = await fetch('/api/cars/brands'); + + if (!response.ok) { + throw new Error('Failed to fetch brands'); + } + + return await response.json(); + } catch (error) { + console.error('Error fetching brands:', error); + return []; + } + } +} \ No newline at end of file diff --git a/src/backend/models/Brand.ts b/src/backend/models/Brand.ts new file mode 100644 index 0000000..6f1906c --- /dev/null +++ b/src/backend/models/Brand.ts @@ -0,0 +1,13 @@ +import { CarModel } from './CarModel'; + +export interface Brand { + id: string; + name: string; + logo: string; + description: string; + foundedYear: number; + headquarters: string; + website: string; + carModels?: CarModel[]; +} + diff --git a/src/backend/models/CarModel.ts b/src/backend/models/CarModel.ts new file mode 100644 index 0000000..fc014d4 --- /dev/null +++ b/src/backend/models/CarModel.ts @@ -0,0 +1,14 @@ +import { Brand } from './Brand'; +import { CarRevision } from './CarRevision'; + +export interface CarModel { + id: string; + name: string; + brand: Brand; + productionStartYear: number; + productionEndYear?: number; + category?: string; + description?: string; + image?: string; + revisions?: CarRevision[]; +} \ No newline at end of file diff --git a/src/backend/models/CarRevision.ts b/src/backend/models/CarRevision.ts new file mode 100644 index 0000000..d50c7db --- /dev/null +++ b/src/backend/models/CarRevision.ts @@ -0,0 +1,25 @@ +import { CarModel } from './CarModel'; + +export interface Dimensions { + length: number; + width: number; + height: number; + wheelbase: number; +} + +export interface CarRevision { + id: string; + name: string; + baseModel: CarModel; + releaseYear: number; + engineTypes: string[]; + horsePower: number; + torque: number; + topSpeed: number; + acceleration0To100: number; + fuelConsumption: number; + dimensions: Dimensions; + weight: number; + features: string[]; + images: string[]; +} \ No newline at end of file diff --git a/src/backend/models/index.ts b/src/backend/models/index.ts new file mode 100644 index 0000000..8fd7eec --- /dev/null +++ b/src/backend/models/index.ts @@ -0,0 +1,3 @@ +export * from './Brand'; +export * from './CarModel'; +export * from './CarRevision'; \ No newline at end of file diff --git a/src/backend/repositories/CarRepository.ts b/src/backend/repositories/CarRepository.ts new file mode 100644 index 0000000..177de87 --- /dev/null +++ b/src/backend/repositories/CarRepository.ts @@ -0,0 +1,512 @@ +import { Brand, CarModel, CarRevision } from '../models'; + +// 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 { + /** + * Get all brands + */ + getAllBrands(): Promise { + return Promise.resolve(brands); + } + + /** + * Get brand by ID + */ + getBrandById(id: string): Promise { + const brand = brands.find(brand => brand.id === id); + return Promise.resolve(brand || null); + } + + /** + * Get all car models + */ + getAllCarModels(): Promise { + return Promise.resolve(carModels); + } + + /** + * Get car models by brand ID + */ + getCarModelsByBrandId(brandId: string): Promise { + const brand = brands.find(brand => brand.id === brandId); + return Promise.resolve(brand?.carModels || []); + } + + /** + * Get car model by ID + */ + getCarModelById(id: string): Promise { + const carModel = carModels.find(model => model.id === id); + return Promise.resolve(carModel || null); + } + + /** + * Get all car revisions + */ + getAllCarRevisions(): Promise { + return Promise.resolve(carRevisions); + } + + /** + * Get car revisions by model ID + */ + getCarRevisionsByModelId(modelId: string): Promise { + const carModel = carModels.find(model => model.id === modelId); + return Promise.resolve(carModel?.revisions || []); + } + + /** + * Get car revision by ID + */ + getCarRevisionById(id: string): Promise { + const carRevision = carRevisions.find(revision => revision.id === id); + return Promise.resolve(carRevision || null); + } + + /** + * Search cars by name (searches both models and revisions) + */ + searchCarsByName(name: string): Promise<{ models: CarModel[], revisions: CarRevision[] }> { + const lowercaseName = name.toLowerCase(); + + const matchingModels = carModels.filter(model => + model.name.toLowerCase().includes(lowercaseName) + ); + + const matchingRevisions = carRevisions.filter(revision => + revision.name.toLowerCase().includes(lowercaseName) + ); + + return Promise.resolve({ + models: matchingModels, + revisions: matchingRevisions + }); + } + + /** + * Get cars by category + */ + getCarsByCategory(category: string): Promise { + const matchingModels = carModels.filter(model => + model.category?.toLowerCase() === category.toLowerCase() + ); + + return Promise.resolve(matchingModels); + } + + /** + * 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; + }); + + return Promise.resolve(matchingModels); + } +} \ No newline at end of file diff --git a/src/backend/repositories/index.ts b/src/backend/repositories/index.ts new file mode 100644 index 0000000..9e4feb8 --- /dev/null +++ b/src/backend/repositories/index.ts @@ -0,0 +1 @@ +export * from './CarRepository'; \ No newline at end of file