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