cleaned up
This commit is contained in:
18
angular.json
18
angular.json
@@ -40,6 +40,10 @@
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.production.ts"
|
||||
},
|
||||
{
|
||||
"replace": "src/app/interceptors/mock-data.interceptor.ts",
|
||||
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
@@ -49,7 +53,7 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumWarning": "600kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
@@ -92,6 +96,10 @@
|
||||
{
|
||||
"replace": "src/app/brands/brand-routes.ts",
|
||||
"with": "src/app/brands/brand-routes.novo.ts"
|
||||
},
|
||||
{
|
||||
"replace": "src/app/interceptors/mock-data.interceptor.ts",
|
||||
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
|
||||
}
|
||||
],
|
||||
"index": "src/index.novo.html",
|
||||
@@ -124,7 +132,7 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumWarning": "600kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
@@ -158,6 +166,10 @@
|
||||
{
|
||||
"replace": "src/app/brands/brand-routes.ts",
|
||||
"with": "src/app/brands/brand-routes.lavero.ts"
|
||||
},
|
||||
{
|
||||
"replace": "src/app/interceptors/mock-data.interceptor.ts",
|
||||
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
|
||||
}
|
||||
],
|
||||
"index": "src/index.lavero.html",
|
||||
@@ -190,7 +202,7 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumWarning": "600kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
|
||||
585
package-lock.json
generated
585
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -12,7 +12,6 @@
|
||||
"build:dexar": "ng build --configuration=production",
|
||||
"build:novo": "ng build --configuration=novo-production",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"lavero": "ng serve --configuration=lavero --port 4202 --proxy-config proxy.conf.lavero.json",
|
||||
"start:lavero": "ng serve --configuration=lavero --port 4202",
|
||||
"build:lavero": "ng build --configuration=lavero-production"
|
||||
@@ -25,9 +24,7 @@
|
||||
"@angular/compiler": "21.1.5",
|
||||
"@angular/core": "21.1.5",
|
||||
"@angular/forms": "21.1.5",
|
||||
"@angular/material": "21.1.5",
|
||||
"@angular/platform-browser": "21.1.5",
|
||||
"@angular/platform-browser-dynamic": "21.1.5",
|
||||
"@angular/router": "21.1.5",
|
||||
"@angular/service-worker": "21.1.5",
|
||||
"primeicons": "^7.0.0",
|
||||
@@ -40,13 +37,6 @@
|
||||
"@angular/build": "21.1.5",
|
||||
"@angular/cli": "21.1.5",
|
||||
"@angular/compiler-cli": "21.1.5",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.13.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
}
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
<app-footer></app-footer>
|
||||
@defer (on viewport) {
|
||||
<app-footer></app-footer>
|
||||
} @placeholder {
|
||||
<div class="footer-placeholder" aria-hidden="true"></div>
|
||||
}
|
||||
<!-- <app-telegram-login /> -->
|
||||
}
|
||||
@@ -5,6 +5,10 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer-placeholder {
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
.server-check-overlay,
|
||||
.server-error-overlay {
|
||||
display: flex;
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { Component, OnInit, signal, ApplicationRef, inject, DestroyRef } from '@angular/core';
|
||||
import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { BackButtonComponent } from './components/back-button/back-button.component';
|
||||
import { ApiService } from './services';
|
||||
import { interval, concat } from 'rxjs';
|
||||
import { filter, first } from 'rxjs/operators';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@@ -27,7 +27,7 @@ export class App implements OnInit {
|
||||
serverAvailable = signal(false);
|
||||
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private apiService = inject(ApiService);
|
||||
private http = inject(HttpClient);
|
||||
private titleService = inject(Title);
|
||||
private swUpdate = inject(SwUpdate);
|
||||
private appRef = inject(ApplicationRef);
|
||||
@@ -55,7 +55,7 @@ export class App implements OnInit {
|
||||
|
||||
private checkServerHealth(): void {
|
||||
this.checkingServer.set(true);
|
||||
this.apiService.ping()
|
||||
this.http.get<{ message: string }>(`${environment.apiUrl}/ping`)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, Renderer2, inject, DOCUMENT } from '@angular/core';
|
||||
import { Router, RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { CartService, LanguageService } from '../../services';
|
||||
import { CartService } from '../../services/cart.service';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { LogoComponent } from '../logo/logo.component';
|
||||
import { LanguageSelectorComponent } from '../language-selector/language-selector.component';
|
||||
|
||||
@@ -23,7 +23,6 @@ export class TelegramLoginComponent implements OnDestroy {
|
||||
|
||||
private readonly pollIntervalMs = 5000;
|
||||
private pollTimer?: ReturnType<typeof setInterval>;
|
||||
private telegramFallbackTimer?: ReturnType<typeof setTimeout>;
|
||||
private readonly handleVisibilityChange = () => {
|
||||
if (typeof document !== 'undefined' && document.visibilityState === 'visible') {
|
||||
this.checkLoginAfterReturn();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
export const PAYMENT_POLL_INTERVAL_MS = 5000;
|
||||
export const PAYMENT_MAX_CHECKS = 36;
|
||||
export const PAYMENT_TIMEOUT_CLOSE_MS = 3000;
|
||||
export const PAYMENT_ERROR_CLOSE_MS = 4000;
|
||||
export const LINK_COPIED_DURATION_MS = 2000;
|
||||
|
||||
// Infinite scroll
|
||||
@@ -12,7 +11,6 @@ export const ITEMS_PER_PAGE = 50;
|
||||
|
||||
// Search
|
||||
export const SEARCH_DEBOUNCE_MS = 300;
|
||||
export const SEARCH_MIN_LENGTH = 3;
|
||||
|
||||
// Cache
|
||||
export const CACHE_DURATION_MS = 5 * 60 * 1000;
|
||||
|
||||
@@ -50,11 +50,6 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
);
|
||||
};
|
||||
|
||||
/** Clear all cached responses */
|
||||
export function clearCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
function cleanupExpiredCache(): void {
|
||||
const now = Date.now();
|
||||
for (const [url, data] of cache.entries()) {
|
||||
|
||||
3
src/app/interceptors/mock-data.interceptor.production.ts
Normal file
3
src/app/interceptors/mock-data.interceptor.production.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
|
||||
export const mockDataInterceptor: HttpInterceptorFn = (req, next) => next(req);
|
||||
@@ -12,14 +12,4 @@ export interface WebSessionStart {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface TelegramAuthData {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
photo_url?: string;
|
||||
auth_date: number;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export type AuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated';
|
||||
|
||||
@@ -33,6 +33,6 @@ export interface Subcategory {
|
||||
subcategories?: Subcategory[];
|
||||
}
|
||||
|
||||
export interface CategoryTranslation {
|
||||
interface CategoryTranslation {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface Photo {
|
||||
interface Photo {
|
||||
photo?: string;
|
||||
video?: string;
|
||||
url: string;
|
||||
@@ -10,7 +10,7 @@ export interface DescriptionField {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
interface Comment {
|
||||
id?: string;
|
||||
text: string;
|
||||
author?: string;
|
||||
@@ -18,13 +18,13 @@ export interface Comment {
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface ItemTranslation {
|
||||
interface ItemTranslation {
|
||||
name?: string;
|
||||
simpleDescription?: string;
|
||||
description?: DescriptionField[];
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
interface Review {
|
||||
rating?: number;
|
||||
content?: string;
|
||||
userID?: string;
|
||||
@@ -32,10 +32,7 @@ export interface Review {
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link Review} instead */
|
||||
export type Callback = Review;
|
||||
|
||||
export interface Question {
|
||||
interface Question {
|
||||
question: string;
|
||||
answer: string;
|
||||
upvotes: number;
|
||||
@@ -51,13 +48,13 @@ export interface ItemName {
|
||||
}
|
||||
|
||||
/** Localized description entry from backend */
|
||||
export interface ItemDescription {
|
||||
interface ItemDescription {
|
||||
language: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** Key-value attribute pair */
|
||||
export interface ItemAttribute {
|
||||
interface ItemAttribute {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
@@ -68,7 +65,7 @@ export interface DeliveryOption {
|
||||
deliveryTime: string;
|
||||
}
|
||||
|
||||
export type DeliveryMode = 'selectable' | 'digital';
|
||||
type DeliveryMode = 'selectable' | 'digital';
|
||||
|
||||
/** Item variant detail (price, size, colour per variant) */
|
||||
export interface ItemDetail {
|
||||
|
||||
@@ -112,7 +112,6 @@ export class CartComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
onSwipeStart(itemID: number, event: TouchEvent): void {
|
||||
const item = event.currentTarget as HTMLElement;
|
||||
const startX = event.touches[0].clientX;
|
||||
|
||||
const onMove = (e: TouchEvent) => {
|
||||
|
||||
@@ -121,11 +121,11 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
// TrackBy function for performance optimization
|
||||
trackByCategoryId(index: number, category: Category): number {
|
||||
trackByCategoryId(_index: number, category: Category): number {
|
||||
return category.categoryID;
|
||||
}
|
||||
|
||||
trackBySubId(index: number, sub: Subcategory): string {
|
||||
trackBySubId(_index: number, sub: Subcategory): string {
|
||||
return sub.id;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { SecurityContext } from '@angular/core';
|
||||
import { getDiscountedPrice, getAllImages, getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
|
||||
import { getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
@@ -297,7 +297,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
this.apiService.submitReview(reviewData).subscribe({
|
||||
next: (response) => {
|
||||
next: () => {
|
||||
this.reviewSubmitStatus.set('success');
|
||||
this.newReview = { rating: 0, comment: '', anonymous: false };
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Observable, timer } from 'rxjs';
|
||||
import { map, retry } from 'rxjs/operators';
|
||||
import { Category, DeliveryOption, Item, Subcategory } from '../models';
|
||||
import { Category, DeliveryOption, Item } from '../models';
|
||||
import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
export interface QrCreateRequest {
|
||||
@@ -69,7 +70,7 @@ export class ApiService {
|
||||
|
||||
private readonly retryConfig = {
|
||||
count: 2,
|
||||
delay: (error: unknown, retryCount: number) => timer(Math.pow(2, retryCount) * 500)
|
||||
delay: (_error: unknown, retryCount: number) => timer(Math.pow(2, retryCount) * 500)
|
||||
};
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
@@ -86,63 +87,6 @@ export class ApiService {
|
||||
return c.startsWith('0x') ? '#' + c.slice(2) : c;
|
||||
}
|
||||
|
||||
private normalizeOptionalNumber(value: unknown): number | undefined {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = typeof value === 'number'
|
||||
? value
|
||||
: Number(String(value).replace(',', '.'));
|
||||
|
||||
return Number.isFinite(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
private normalizeOptionalString(value: unknown): string {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'bigint') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
private normalizeDeliveryOption(value: unknown): DeliveryOption | null {
|
||||
const source = this.asRecord(value);
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deliveryPlace = this.normalizeOptionalString(
|
||||
source['deliveryPlace'] ?? source['delivery_place'] ?? source['deliveryplace']
|
||||
);
|
||||
const deliveryTime = this.normalizeOptionalString(
|
||||
source['deliveryTime'] ?? source['delivery_time'] ?? source['deliverytime']
|
||||
);
|
||||
const deliveryPrice = this.normalizeOptionalNumber(
|
||||
source['deliveryPrice'] ?? source['delivery_price'] ?? source['deliveryprice']
|
||||
);
|
||||
|
||||
if (deliveryPrice === undefined && !deliveryPlace && !deliveryTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
deliveryPrice: deliveryPrice ?? 0,
|
||||
deliveryPlace,
|
||||
deliveryTime,
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeDeliveryData(
|
||||
raw: any,
|
||||
legacyDeliveryPrice?: number
|
||||
@@ -159,7 +103,7 @@ export class ApiService {
|
||||
? [rawDelivery]
|
||||
: [];
|
||||
const options = deliveryCandidates
|
||||
.map(candidate => this.normalizeDeliveryOption(candidate))
|
||||
.map(candidate => normalizeDeliveryOption(candidate))
|
||||
.filter((option): option is DeliveryOption => option !== null);
|
||||
|
||||
if (options.length > 0) {
|
||||
@@ -197,7 +141,7 @@ export class ApiService {
|
||||
private normalizeItem(raw: any): Item {
|
||||
const { partnerID, ...rest } = raw;
|
||||
const item: Item = { ...rest };
|
||||
let legacyDeliveryPrice = this.normalizeOptionalNumber(
|
||||
let legacyDeliveryPrice = normalizeOptionalNumber(
|
||||
raw.deliveryPrice ?? raw.delivery_price ?? raw.deliveryprice
|
||||
);
|
||||
|
||||
@@ -210,7 +154,7 @@ export class ApiService {
|
||||
...d,
|
||||
colour: this.normalizeColor(d.colour || d.color || ''),
|
||||
color: undefined,
|
||||
deliveryPrice: this.normalizeOptionalNumber(
|
||||
deliveryPrice: normalizeOptionalNumber(
|
||||
d.deliveryPrice ?? d.delivery_price ?? d.deliveryprice
|
||||
),
|
||||
}));
|
||||
@@ -219,7 +163,7 @@ export class ApiService {
|
||||
if (!item.colour) item.colour = this.normalizeColor(detail.colour || detail.color || '');
|
||||
if (!item.size) item.size = detail.size || '';
|
||||
if (legacyDeliveryPrice === undefined) {
|
||||
legacyDeliveryPrice = this.normalizeOptionalNumber(
|
||||
legacyDeliveryPrice = normalizeOptionalNumber(
|
||||
detail.deliveryPrice ?? detail.delivery_price ?? detail.deliveryprice
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, signal, computed, effect } from '@angular/core';
|
||||
import { ApiService } from './api.service';
|
||||
import { DeliveryOption, Item, CartItem } from '../models';
|
||||
import { Injectable, signal, computed, effect, Injector } from '@angular/core';
|
||||
import { DeliveryOption, CartItem } from '../models';
|
||||
import { getDiscountedPrice } from '../utils/item.utils';
|
||||
import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils';
|
||||
import { environment } from '../../environments/environment';
|
||||
import type { } from '../types/telegram.types';
|
||||
|
||||
@@ -48,7 +48,7 @@ export class CartService {
|
||||
&& items.some(item => (item.deliveryOptions?.length ?? 0) > 0 || item.selectedDelivery != null);
|
||||
});
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
constructor(private injector: Injector) {
|
||||
this.loadCart();
|
||||
|
||||
// Auto-save whenever cart changes (skip the initial empty state)
|
||||
@@ -60,57 +60,6 @@ export class CartService {
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeOptionalNumber(value: unknown): number | undefined {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = typeof value === 'number'
|
||||
? value
|
||||
: Number(String(value).replace(',', '.'));
|
||||
|
||||
return Number.isFinite(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
private normalizeOptionalString(value: unknown): string {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'bigint') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private normalizeDeliveryOption(value: unknown): DeliveryOption | null {
|
||||
if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = value as Record<string, unknown>;
|
||||
const deliveryPlace = this.normalizeOptionalString(
|
||||
source['deliveryPlace'] ?? source['delivery_place'] ?? source['deliveryplace']
|
||||
);
|
||||
const deliveryTime = this.normalizeOptionalString(
|
||||
source['deliveryTime'] ?? source['delivery_time'] ?? source['deliverytime']
|
||||
);
|
||||
const deliveryPrice = this.normalizeOptionalNumber(
|
||||
source['deliveryPrice'] ?? source['delivery_price'] ?? source['deliveryprice']
|
||||
);
|
||||
|
||||
if (deliveryPrice === undefined && !deliveryPlace && !deliveryTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
deliveryPrice: deliveryPrice ?? 0,
|
||||
deliveryPlace,
|
||||
deliveryTime,
|
||||
};
|
||||
}
|
||||
|
||||
private sameDeliveryOption(left: DeliveryOption, right: DeliveryOption): boolean {
|
||||
return left.deliveryPrice === right.deliveryPrice
|
||||
&& left.deliveryPlace === right.deliveryPlace
|
||||
@@ -131,10 +80,10 @@ export class CartService {
|
||||
} {
|
||||
const normalizedOptions = Array.isArray(item.deliveryOptions)
|
||||
? item.deliveryOptions
|
||||
.map(option => this.normalizeDeliveryOption(option))
|
||||
.map(option => normalizeDeliveryOption(option))
|
||||
.filter((option): option is DeliveryOption => option !== null)
|
||||
: [];
|
||||
const legacyDeliveryPrice = this.normalizeOptionalNumber(item.deliveryPrice);
|
||||
const legacyDeliveryPrice = normalizeOptionalNumber(item.deliveryPrice);
|
||||
const deliveryOptions = normalizedOptions.length > 0
|
||||
? normalizedOptions
|
||||
: legacyDeliveryPrice !== undefined
|
||||
@@ -148,7 +97,7 @@ export class CartService {
|
||||
const deliverySelectionRequired = deliveryOptions.length > 0
|
||||
? item.deliverySelectionRequired !== false && normalizedOptions.length > 0
|
||||
: false;
|
||||
const selectedDelivery = this.normalizeDeliveryOption(item.selectedDelivery)
|
||||
const selectedDelivery = normalizeDeliveryOption(item.selectedDelivery)
|
||||
?? (deliveryOptions.length === 1 && !deliverySelectionRequired ? deliveryOptions[0] : null);
|
||||
const matchedSelection = selectedDelivery && deliveryOptions.length > 0
|
||||
? deliveryOptions.find(option => this.sameDeliveryOption(option, selectedDelivery)) ?? selectedDelivery
|
||||
@@ -251,23 +200,28 @@ export class CartService {
|
||||
} else {
|
||||
// Get item details from API and add to cart
|
||||
this.addingItems.add(itemID);
|
||||
this.apiService.getItem(itemID).subscribe({
|
||||
next: (item) => {
|
||||
const cartItem = this.normalizeCartItem({
|
||||
...item,
|
||||
quantity,
|
||||
...(variant?.colour != null && { colour: variant.colour }),
|
||||
...(variant?.size != null && { size: variant.size }),
|
||||
...(variant?.price != null && { price: variant.price }),
|
||||
...(variant?.currency != null && { currency: variant.currency }),
|
||||
});
|
||||
this.cartItems.set([...this.cartItems(), cartItem]);
|
||||
this.addingItems.delete(itemID);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error adding to cart:', err);
|
||||
this.addingItems.delete(itemID);
|
||||
}
|
||||
import('./api.service').then(({ ApiService }) => {
|
||||
this.injector.get(ApiService).getItem(itemID).subscribe({
|
||||
next: (item) => {
|
||||
const cartItem = this.normalizeCartItem({
|
||||
...item,
|
||||
quantity,
|
||||
...(variant?.colour != null && { colour: variant.colour }),
|
||||
...(variant?.size != null && { size: variant.size }),
|
||||
...(variant?.price != null && { price: variant.price }),
|
||||
...(variant?.currency != null && { currency: variant.currency }),
|
||||
});
|
||||
this.cartItems.set([...this.cartItems(), cartItem]);
|
||||
this.addingItems.delete(itemID);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error adding to cart:', err);
|
||||
this.addingItems.delete(itemID);
|
||||
}
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error('Error loading API service:', err);
|
||||
this.addingItems.delete(itemID);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -286,7 +240,7 @@ export class CartService {
|
||||
}
|
||||
|
||||
setSelectedDelivery(itemID: number, selectedDelivery: DeliveryOption | null): void {
|
||||
const normalizedSelection = this.normalizeDeliveryOption(selectedDelivery);
|
||||
const normalizedSelection = normalizeDeliveryOption(selectedDelivery);
|
||||
const updatedItems = this.cartItems().map(item => {
|
||||
if (item.itemID !== itemID) {
|
||||
return item;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface TelegramUser {
|
||||
interface TelegramUser {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
@@ -21,3 +21,5 @@ declare global {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -13,25 +13,10 @@ export function getMainImage(item: Item): string {
|
||||
return item.photos?.[0]?.url || '/assets/images/placeholder.svg';
|
||||
}
|
||||
|
||||
export function getAllImages(item: Item): string[] {
|
||||
if (item.imgs && item.imgs.length > 0) {
|
||||
return item.imgs;
|
||||
}
|
||||
return item.photos?.map(p => p.url) || [];
|
||||
}
|
||||
|
||||
export function trackByItemId(index: number, item: Item): number | string {
|
||||
export function trackByItemId(_index: number, item: Item): number | string {
|
||||
return item.id || item.itemID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display description — supports both legacy HTML string
|
||||
* and structured key-value pairs from backOffice API.
|
||||
*/
|
||||
export function hasStructuredDescription(item: Item): boolean {
|
||||
return Array.isArray(item.descriptionFields) && item.descriptionFields.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute stock status from quantity if the legacy `remainings` field is absent.
|
||||
*/
|
||||
|
||||
58
src/app/utils/normalization.utils.ts
Normal file
58
src/app/utils/normalization.utils.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { DeliveryOption } from '../models';
|
||||
|
||||
export function normalizeOptionalNumber(value: unknown): number | undefined {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = typeof value === 'number'
|
||||
? value
|
||||
: Number(String(value).replace(',', '.'));
|
||||
|
||||
return Number.isFinite(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeOptionalString(value: unknown): string {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'bigint') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
export function normalizeDeliveryOption(value: unknown): DeliveryOption | null {
|
||||
const source = asRecord(value);
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deliveryPlace = normalizeOptionalString(
|
||||
source['deliveryPlace'] ?? source['delivery_place'] ?? source['deliveryplace']
|
||||
);
|
||||
const deliveryTime = normalizeOptionalString(
|
||||
source['deliveryTime'] ?? source['delivery_time'] ?? source['deliverytime']
|
||||
);
|
||||
const deliveryPrice = normalizeOptionalNumber(
|
||||
source['deliveryPrice'] ?? source['delivery_price'] ?? source['deliveryprice']
|
||||
);
|
||||
|
||||
if (deliveryPrice === undefined && !deliveryPlace && !deliveryTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
deliveryPrice: deliveryPrice ?? 0,
|
||||
deliveryPlace,
|
||||
deliveryTime,
|
||||
};
|
||||
}
|
||||
@@ -5,12 +5,6 @@
|
||||
|
||||
/* Google Fonts loaded via <link> in index.html for non-blocking rendering */
|
||||
|
||||
/* Font optimization */
|
||||
@font-face {
|
||||
font-family: system-ui;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Default CSS Variables - will be overridden by theme files */
|
||||
:root {
|
||||
--primary-color: #497671;
|
||||
@@ -191,6 +185,7 @@ a, button, input, textarea, select {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"rootDir": "./src",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user