diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..096746c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ \ No newline at end of file diff --git a/src/app/core/interfaces/vehicle.interface.ts b/src/app/core/interfaces/vehicle.interface.ts new file mode 100644 index 0000000..36cb5a1 --- /dev/null +++ b/src/app/core/interfaces/vehicle.interface.ts @@ -0,0 +1,42 @@ +export interface VehicleMedia { + name: string; + url: string; +} + +export interface VehicleEmissions { + template: string; + value: number; +} + +export interface VehicleMeta { + passengers: number; + drivetrain: string[]; + bodystyles: string[]; + emissions: VehicleEmissions; +} + +/** Shape returned by GET /api/vehicles/ */ +export interface VehicleSummary { + id: string; + name: string; + modelYear: string; + apiUrl: string; + media: VehicleMedia[]; +} + +/** Shape returned by GET /api/vehicles/:id */ +export interface VehicleDetail { + id: string; + description: string; + price: string; + meta: VehicleMeta; +} + +/** Merged type used throughout the application */ +export interface Vehicle extends VehicleSummary, VehicleDetail {} + +/** + * Discriminated union — extend with Motorbike when the API supports it. + * e.g. type AnyVehicle = Car | Motorbike; + */ +export type AnyVehicle = Vehicle; diff --git a/src/app/core/services/vehicle.service.ts b/src/app/core/services/vehicle.service.ts new file mode 100644 index 0000000..6116ee4 --- /dev/null +++ b/src/app/core/services/vehicle.service.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable, signal } from '@angular/core'; +import { + EMPTY, + Observable, + catchError, + forkJoin, + map, + switchMap, +} from 'rxjs'; +import { + AnyVehicle, + VehicleDetail, + VehicleSummary, +} from '../interfaces/vehicle.interface'; + +const API_BASE = + 'https://frontend-code-test-api-1023992580432.europe-west2.run.app'; + +@Injectable({ providedIn: 'root' }) +export class VehicleService { + readonly loading = signal(false); + readonly error = signal(null); + + constructor(private readonly http: HttpClient) {} + + /** + * Fetches the vehicle list then retrieves all detail records in parallel. + * Uses switchMap so any re-trigger cancels the previous in-flight request, + * and forkJoin because HTTP calls are one-shot (complete after one emission). + */ + fetchAll(): Observable { + this.loading.set(true); + this.error.set(null); + + return this.http.get(`${API_BASE}/api/vehicles/`).pipe( + switchMap((summaries) => + forkJoin( + summaries.map((summary) => + this.http + .get(`${API_BASE}${summary.apiUrl}`) + .pipe( + map((detail) => this.merge(summary, detail)), + // Per-vehicle errors are swallowed so one bad record + // (e.g. "problematic") doesn't kill the entire list. + catchError(() => EMPTY), + ), + ), + ), + ), + map((vehicles) => { + this.loading.set(false); + return vehicles; + }), + catchError((err: Error) => { + this.loading.set(false); + this.error.set(err.message ?? 'Failed to load vehicles'); + return EMPTY; + }), + ); + } + + private merge(summary: VehicleSummary, detail: VehicleDetail): AnyVehicle { + return { ...summary, ...detail }; + } +}