diff --git a/src/app/app.html b/src/app/app.html index 7528372..fa8f995 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,342 +1,3 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
+
+
- - - - - - - - - - - diff --git a/src/app/app.scss b/src/app/app.scss index e69de29..476ea8d 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -0,0 +1,20 @@ +:host { + display: flex; + flex-direction: column; + height: 100vh; + height: 100dvh; // better support on phone browsers +} + + +.app-content { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + max-width: 1200px; + width: 100%; + box-sizing: border-box; + margin: 0 auto; + padding: 1.5rem; + overflow-y: auto; +} diff --git a/src/app/core/models/product.model.ts b/src/app/core/models/product.model.ts new file mode 100644 index 0000000..239319a --- /dev/null +++ b/src/app/core/models/product.model.ts @@ -0,0 +1,48 @@ +// Raw Product Model +export interface Product { + id: string; + brand: string; + model: string; + price: string; + imgUrl: string; +} + + +export interface ProductOption { + code: number; + name: string; +} + +// product options for +export interface ProductOptions { + colors: ProductOption[]; + storages: ProductOption[]; +} + +// Raw item detail structure +export interface ProductDetail extends Product { + cpu?: string; + ram?: string; + os?: string; + displayType?: string; + displayResolution?: string; + displaySize?: string; + battery?: string; + primaryCamera?: string[]; + secondaryCmera?: string[]; + dimentions?: string; + weight?: string; + options: ProductOptions; +} + +// Requests and responses for adding a product to the cart (only for demo purposes, no extended functionality to implement +export interface AddCartRequest { + id: string; + colorCode: number; + storageCode: number; +} + +// Response sent when adding in a chart (total numeber of items added) +export interface AddCartResponse { + count: number; +} diff --git a/src/app/core/services/cache.service.ts b/src/app/core/services/cache.service.ts new file mode 100644 index 0000000..7fb411d --- /dev/null +++ b/src/app/core/services/cache.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; + +// Cache expiration time. (In a real shop this should not be the beast approach due to stock changes) +const CACHE_TTL_MS = 60 * 60 * 1000; + +// Prefix for cache keys to avoid collisions with other localStorage entries. +const CACHE_PREFIX = 'itx-cache:'; + +interface CacheEntry { + timestamp: number; + data: T; +} + +// Generic cache service +@Injectable({ providedIn: 'root' }) +export class CacheService { + // returns cached item if it exists and is not expired, otherwise fetches it using the provided fetcher function. + async getOrFetch(key: string, fetcher: () => Promise): Promise { + const cached = this.read(key); + if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { + return cached.data; + } + + const data = await fetcher(); + this.write(key, data); + return data; + } + + // Clears all cache entries managed by this service. + clear(): void { + Object.keys(localStorage) + .filter((key) => key.startsWith(CACHE_PREFIX)) + .forEach((key) => localStorage.removeItem(key)); + } + + private read(key: string): CacheEntry | null { + const raw = localStorage.getItem(CACHE_PREFIX + key); + if (!raw) { + return null; + } + try { + return JSON.parse(raw) as CacheEntry; + } catch { + // corrupted data + return null; + } + } + + private write(key: string, data: T): void { + const entry: CacheEntry = { timestamp: Date.now(), data }; + localStorage.setItem(CACHE_PREFIX + key, JSON.stringify(entry)); + } +} diff --git a/src/app/core/services/cart.service.ts b/src/app/core/services/cart.service.ts new file mode 100644 index 0000000..6c23fcb --- /dev/null +++ b/src/app/core/services/cart.service.ts @@ -0,0 +1,38 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +import { AddCartRequest, AddCartResponse } from '../models/product.model'; +import { environment } from '../../../environments/environment'; + +const API_BASE_URL = environment.apiBaseUrl; +const CART_COUNT_STORAGE_KEY = environment.cartCountStorageKey; + +// Add cart implementation (just for visualization, not used in the app) +@Injectable({ providedIn: 'root' }) +export class CartService { + private readonly http = inject(HttpClient); + + //total items on cart + readonly count = signal(this.readPersistedCount()); + + async addToCart(request: AddCartRequest): Promise { + const response = await firstValueFrom( + this.http.post(`${API_BASE_URL}/cart`, request) + ); + this.setCount(response.count); + return response.count; + } + + // reloads counter from localstorage + private readPersistedCount(): number { + const raw = localStorage.getItem(CART_COUNT_STORAGE_KEY); + const parsed = raw ? Number(raw) : 0; + return Number.isFinite(parsed) ? parsed : 0; + } + + private setCount(count: number): void { + this.count.set(count); + localStorage.setItem(CART_COUNT_STORAGE_KEY, String(count)); + } +} diff --git a/src/app/core/services/product.service.ts b/src/app/core/services/product.service.ts new file mode 100644 index 0000000..79a8d78 --- /dev/null +++ b/src/app/core/services/product.service.ts @@ -0,0 +1,28 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +import { CacheService } from './cache.service'; +import { Product, ProductDetail } from '../models/product.model'; +import { environment } from '../../../environments/environment'; + +const API_BASE_URL = environment.apiBaseUrl; + + +@Injectable({ providedIn: 'root' }) +export class ProductService { + private readonly http = inject(HttpClient); + private readonly cache = inject(CacheService); + + getProducts(): Promise { + const url = `${API_BASE_URL}/product`; + return this.cache.getOrFetch(url, () => firstValueFrom(this.http.get(url))); + } + + getProductDetail(id: string): Promise { + const url = `${API_BASE_URL}/product/${id}`; + return this.cache.getOrFetch(url, () => + firstValueFrom(this.http.get(url)) + ); + } +} diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts new file mode 100644 index 0000000..131dcd5 --- /dev/null +++ b/src/environments/environment.development.ts @@ -0,0 +1,6 @@ +export const environment = { + production: false, + apiBaseUrl: 'https://itx-frontend-test.onrender.com/api', + cartCountStorageKey: 'itx-cart-count', +}; + diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts new file mode 100644 index 0000000..1fcbadb --- /dev/null +++ b/src/environments/environment.production.ts @@ -0,0 +1,6 @@ +export const environment = { + production: true, + apiBaseUrl: 'https://itx-frontend-test.onrender.com/api', + cartCountStorageKey: 'itx-cart-count', +}; + diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..131dcd5 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,6 @@ +export const environment = { + production: false, + apiBaseUrl: 'https://itx-frontend-test.onrender.com/api', + cartCountStorageKey: 'itx-cart-count', +}; +