inital commit
This commit is contained in:
parent
27bc5d0114
commit
d028c67c68
15
.cursor/rules/instructions.mdc
Normal file
15
.cursor/rules/instructions.mdc
Normal 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
14
.cursor/rules/project.mdc
Normal 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
10
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
22
src/app/api/cars/brand/[id]/route.ts
Normal file
22
src/app/api/cars/brand/[id]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/app/api/cars/brands/route.ts
Normal file
17
src/app/api/cars/brands/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
39
src/app/api/cars/search/route.ts
Normal file
39
src/app/api/cars/search/route.ts
Normal 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
201
src/app/components.css
Normal 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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
180
src/app/home.module.css
Normal file
180
src/app/home.module.css
Normal 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);
|
||||
}
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
140
src/app/page.tsx
140
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 (
|
||||
<div className={styles.page}>
|
||||
<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>
|
||||
const [query, setQuery] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
<div className={styles.ctas}>
|
||||
<a
|
||||
className={styles.primary}
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.secondary}
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (query.trim()) {
|
||||
router.push(`/results?query=${encodeURIComponent(query)}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className={styles.container}>
|
||||
<h1 className={styles.title}>
|
||||
<span className={styles.gradientText}>E-WIKI</span>
|
||||
</h1>
|
||||
|
||||
<div className={styles.searchContainer}>
|
||||
<form onSubmit={handleSubmit} className={styles.searchForm}>
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search for electric vehicles..."
|
||||
className={styles.searchInput}
|
||||
aria-label="Search for electric vehicles"
|
||||
/>
|
||||
<button type="submit" className={styles.searchButton}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style={{ marginRight: '8px' }}>
|
||||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
||||
</svg>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className={styles.brandSection}>
|
||||
<p>Popular brands</p>
|
||||
<div className={styles.brandList}>
|
||||
<Link href="/results?brand=1" className={styles.brandLink}>Tesla</Link>
|
||||
<Link href="/results?brand=2" className={styles.brandLink}>BMW</Link>
|
||||
<Link href="/results?brand=3" className={styles.brandLink}>Toyota</Link>
|
||||
<Link href="/results?brand=4" className={styles.brandLink}>Audi</Link>
|
||||
<Link href="/results?brand=5" className={styles.brandLink}>Mercedes</Link>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
210
src/app/results/page.tsx
Normal file
210
src/app/results/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
209
src/app/results/results.module.css
Normal file
209
src/app/results/results.module.css
Normal 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;
|
||||
}
|
||||
80
src/app/services/carService.ts
Normal file
80
src/app/services/carService.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/backend/models/Brand.ts
Normal file
13
src/backend/models/Brand.ts
Normal 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[];
|
||||
}
|
||||
|
||||
14
src/backend/models/CarModel.ts
Normal file
14
src/backend/models/CarModel.ts
Normal 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[];
|
||||
}
|
||||
25
src/backend/models/CarRevision.ts
Normal file
25
src/backend/models/CarRevision.ts
Normal 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[];
|
||||
}
|
||||
3
src/backend/models/index.ts
Normal file
3
src/backend/models/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './Brand';
|
||||
export * from './CarModel';
|
||||
export * from './CarRevision';
|
||||
512
src/backend/repositories/CarRepository.ts
Normal file
512
src/backend/repositories/CarRepository.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
1
src/backend/repositories/index.ts
Normal file
1
src/backend/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './CarRepository';
|
||||
Loading…
x
Reference in New Issue
Block a user