inital commit

This commit is contained in:
Tim Lappe 2025-05-19 08:42:49 +02:00
parent 27bc5d0114
commit d028c67c68
22 changed files with 1944 additions and 104 deletions

View File

@ -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

14
.cursor/rules/project.mdc Normal file
View File

@ -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

10
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "evwiki", "name": "evwiki",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@heroicons/react": "^2.2.0",
"next": "15.3.2", "next": "15.3.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
@ -196,6 +197,15 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "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": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",

View File

@ -9,17 +9,18 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.2.0",
"next": "15.3.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0"
"next": "15.3.2"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@eslint/eslintrc": "^3",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.2", "eslint-config-next": "15.3.2",
"@eslint/eslintrc": "^3" "typescript": "^5"
} }
} }

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

201
src/app/components.css Normal file
View File

@ -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);
}

View File

@ -1,13 +1,16 @@
:root { :root {
--background: #ffffff; --color-sunset: #F6D8AE;
--foreground: #171717; --color-charcoal: #2E4057;
} --color-yale-blue: #083D77;
--color-cerise: #DA4167;
--color-naples-yellow: #F4D35E;
@media (prefers-color-scheme: dark) { /* Neutral background colors */
:root { --color-light-neutral: #F5F5F5;
--background: #0a0a0a; --color-dark-neutral: #1F1F1F;
--foreground: #ededed;
} --background: var(--color-light-neutral);
--foreground: var(--color-charcoal);
} }
html, html,
@ -17,8 +20,8 @@ body {
} }
body { body {
color: var(--foreground);
background: var(--background); background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@ -40,3 +43,8 @@ a {
color-scheme: dark; color-scheme: dark;
} }
} }
.button-primary {
background: var(--color-yale-blue);
color: #fff;
}

180
src/app/home.module.css Normal file
View File

@ -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);
}

View File

@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import "./components.css";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "EV WIKI",
description: "Generated by create next app", description: "Modern search engine for electric vehicle information",
}; };
export default function RootLayout({ export default function RootLayout({

View File

@ -166,3 +166,306 @@ a.secondary {
filter: invert(); 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;
}

View File

@ -1,95 +1,57 @@
import Image from "next/image"; 'use client';
import styles from "./page.module.css";
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() { export default function Home() {
return ( const [query, setQuery] = useState('');
<div className={styles.page}> const router = useRouter();
<main className={styles.main}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol>
<li>
Get started by editing <code>src/app/page.tsx</code>.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className={styles.ctas}> const handleSubmit = (e: FormEvent) => {
<a e.preventDefault();
className={styles.primary}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app" if (query.trim()) {
target="_blank" router.push(`/results?query=${encodeURIComponent(query)}`);
rel="noopener noreferrer" }
> };
<Image
className={styles.logo} return (
src="/vercel.svg" <main className={styles.container}>
alt="Vercel logomark" <h1 className={styles.title}>
width={20} <span className={styles.gradientText}>E-WIKI</span>
height={20} </h1>
<div className={styles.searchContainer}>
<form onSubmit={handleSubmit} className={styles.searchForm}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for electric vehicles..."
className={styles.searchInput}
aria-label="Search for electric vehicles"
/> />
Deploy now <button type="submit" className={styles.searchButton}>
</a> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style={{ marginRight: '8px' }}>
<a <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app" </svg>
target="_blank" Search
rel="noopener noreferrer" </button>
className={styles.secondary} </form>
> </div>
Read our docs
</a> <div className={styles.brandSection}>
<p>Popular brands</p>
<div className={styles.brandList}>
<Link href="/results?brand=1" className={styles.brandLink}>Tesla</Link>
<Link href="/results?brand=2" className={styles.brandLink}>BMW</Link>
<Link href="/results?brand=3" className={styles.brandLink}>Toyota</Link>
<Link href="/results?brand=4" className={styles.brandLink}>Audi</Link>
<Link href="/results?brand=5" className={styles.brandLink}>Mercedes</Link>
</div>
</div> </div>
</main> </main>
<footer className={styles.footer}>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
); );
} }

210
src/app/results/page.tsx Normal file
View File

@ -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<CarModel[]>([]);
const [revisions, setRevisions] = useState<CarRevision[]>([]);
const [error, setError] = useState<string | null>(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 (
<main className="min-h-screen p-8 bg-gray-50">
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Search Results</h1>
<Link
href="/"
className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors"
>
New Search
</Link>
</div>
{loading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
) : error ? (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">
{error}
</div>
) : models.length === 0 && revisions.length === 0 ? (
<div className="bg-white p-8 rounded-lg shadow-md text-center">
<h2 className="text-2xl font-semibold mb-4">No results found</h2>
<p className="text-gray-600">
Try adjusting your search criteria to find more cars.
</p>
</div>
) : (
<div className="space-y-8">
{models.length > 0 && (
<div>
<h2 className="text-2xl font-semibold mb-4">Car Models</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{models.map((model) => (
<div
key={model.id}
className="bg-white rounded-lg shadow-md overflow-hidden"
>
<div className="h-48 bg-gray-200 relative">
{model.image ? (
<img
src={model.image}
alt={model.name}
className="w-full h-full object-cover"
/>
) : (
<div className="flex items-center justify-center h-full text-gray-500">
No image available
</div>
)}
</div>
<div className="p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold">{model.name}</h3>
<span className="text-sm font-medium text-blue-600">
{model.brand?.name}
</span>
</div>
<p className="text-gray-600 text-sm mb-2">{model.description}</p>
<div className="flex justify-between items-center text-sm">
<span className="bg-gray-100 px-2 py-1 rounded">
{model.category}
</span>
<span>
Since {model.productionStartYear}
{model.productionEndYear
? ` - ${model.productionEndYear}`
: ''}
</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
{revisions.length > 0 && (
<div>
<h2 className="text-2xl font-semibold mb-4">Car Revisions</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{revisions.map((revision) => (
<div
key={revision.id}
className="bg-white rounded-lg shadow-md overflow-hidden"
>
<div className="h-48 bg-gray-200 relative">
{revision.images && revision.images.length > 0 ? (
<img
src={revision.images[0]}
alt={revision.name}
className="w-full h-full object-cover"
/>
) : (
<div className="flex items-center justify-center h-full text-gray-500">
No image available
</div>
)}
</div>
<div className="p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold">{revision.name}</h3>
<span className="text-sm font-medium text-blue-600">
{revision.baseModel?.brand?.name}
</span>
</div>
<div className="grid grid-cols-2 gap-2 mb-2 text-sm">
<div>
<span className="font-medium">Engine: </span>
{revision.engineTypes?.join(', ')}
</div>
<div>
<span className="font-medium">Power: </span>
{revision.horsePower} HP
</div>
<div>
<span className="font-medium">0-100 km/h: </span>
{revision.acceleration0To100}s
</div>
<div>
<span className="font-medium">Top Speed: </span>
{revision.topSpeed} km/h
</div>
</div>
<div className="flex flex-wrap gap-1 mt-2">
{revision.features?.slice(0, 3).map((feature, index) => (
<span
key={index}
className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded"
>
{feature}
</span>
))}
{revision.features && revision.features.length > 3 && (
<span className="text-xs text-gray-500">
+{revision.features.length - 3} more
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</main>
);
}

View File

@ -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;
}

View File

@ -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<SearchResults> {
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<SearchResults> {
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<SearchResults> {
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<Brand[]> {
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 [];
}
}
}

View File

@ -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[];
}

View File

@ -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[];
}

View File

@ -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[];
}

View File

@ -0,0 +1,3 @@
export * from './Brand';
export * from './CarModel';
export * from './CarRevision';

View File

@ -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<Brand[]> {
return Promise.resolve(brands);
}
/**
* Get brand by ID
*/
getBrandById(id: string): Promise<Brand | null> {
const brand = brands.find(brand => brand.id === id);
return Promise.resolve(brand || null);
}
/**
* Get all car models
*/
getAllCarModels(): Promise<CarModel[]> {
return Promise.resolve(carModels);
}
/**
* Get car models by brand ID
*/
getCarModelsByBrandId(brandId: string): Promise<CarModel[]> {
const brand = brands.find(brand => brand.id === brandId);
return Promise.resolve(brand?.carModels || []);
}
/**
* Get car model by ID
*/
getCarModelById(id: string): Promise<CarModel | null> {
const carModel = carModels.find(model => model.id === id);
return Promise.resolve(carModel || null);
}
/**
* Get all car revisions
*/
getAllCarRevisions(): Promise<CarRevision[]> {
return Promise.resolve(carRevisions);
}
/**
* Get car revisions by model ID
*/
getCarRevisionsByModelId(modelId: string): Promise<CarRevision[]> {
const carModel = carModels.find(model => model.id === modelId);
return Promise.resolve(carModel?.revisions || []);
}
/**
* Get car revision by ID
*/
getCarRevisionById(id: string): Promise<CarRevision | null> {
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<CarModel[]> {
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<CarModel[]> {
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);
}
}

View File

@ -0,0 +1 @@
export * from './CarRepository';