feat(front-end): faster load
This commit is contained in:
@@ -1,22 +1,89 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
PLATFORM_ID,
|
||||||
|
TransferState,
|
||||||
|
inject,
|
||||||
|
makeStateKey,
|
||||||
|
} from '@angular/core';
|
||||||
import { TranslateLoader, TranslationObject } from '@ngx-translate/core';
|
import { TranslateLoader, TranslationObject } from '@ngx-translate/core';
|
||||||
import { Observable, of } from 'rxjs';
|
import { from, Observable } from 'rxjs';
|
||||||
import de from '../../../assets/i18n/de.json';
|
|
||||||
import en from '../../../assets/i18n/en.json';
|
|
||||||
import fr from '../../../assets/i18n/fr.json';
|
|
||||||
import it from '../../../assets/i18n/it.json';
|
|
||||||
|
|
||||||
const TRANSLATIONS: Record<string, TranslationObject> = {
|
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
|
||||||
it: it as TranslationObject,
|
|
||||||
en: en as TranslationObject,
|
const FALLBACK_LANG: SupportedLang = 'it';
|
||||||
de: de as TranslationObject,
|
const translationCache = new Map<SupportedLang, Promise<TranslationObject>>();
|
||||||
fr: fr as TranslationObject,
|
|
||||||
|
const translationLoaders: Record<SupportedLang, () => Promise<TranslationObject>> = {
|
||||||
|
it: () =>
|
||||||
|
import('../../../assets/i18n/it.json').then(
|
||||||
|
(module) => module.default as TranslationObject,
|
||||||
|
),
|
||||||
|
en: () =>
|
||||||
|
import('../../../assets/i18n/en.json').then(
|
||||||
|
(module) => module.default as TranslationObject,
|
||||||
|
),
|
||||||
|
de: () =>
|
||||||
|
import('../../../assets/i18n/de.json').then(
|
||||||
|
(module) => module.default as TranslationObject,
|
||||||
|
),
|
||||||
|
fr: () =>
|
||||||
|
import('../../../assets/i18n/fr.json').then(
|
||||||
|
(module) => module.default as TranslationObject,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StaticTranslateLoader implements TranslateLoader {
|
export class StaticTranslateLoader implements TranslateLoader {
|
||||||
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
private readonly transferState = inject(TransferState);
|
||||||
|
|
||||||
getTranslation(lang: string): Observable<TranslationObject> {
|
getTranslation(lang: string): Observable<TranslationObject> {
|
||||||
const normalized = String(lang || 'it').toLowerCase();
|
const normalized = this.normalizeLanguage(lang);
|
||||||
return of(TRANSLATIONS[normalized] ?? TRANSLATIONS['it']);
|
return from(this.loadTranslation(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeLanguage(lang: string): SupportedLang {
|
||||||
|
const normalized = String(lang || FALLBACK_LANG).toLowerCase();
|
||||||
|
return normalized in translationLoaders
|
||||||
|
? (normalized as SupportedLang)
|
||||||
|
: FALLBACK_LANG;
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadTranslation(lang: SupportedLang): Promise<TranslationObject> {
|
||||||
|
const transferStateKey =
|
||||||
|
makeStateKey<TranslationObject>(`i18n:${lang.toLowerCase()}`);
|
||||||
|
if (
|
||||||
|
isPlatformBrowser(this.platformId) &&
|
||||||
|
this.transferState.hasKey(transferStateKey)
|
||||||
|
) {
|
||||||
|
const transferred = this.transferState.get(transferStateKey, {});
|
||||||
|
this.transferState.remove(transferStateKey);
|
||||||
|
return Promise.resolve(transferred);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = translationCache.get(lang);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = translationLoaders[lang]()
|
||||||
|
.then((translation) => {
|
||||||
|
if (
|
||||||
|
isPlatformServer(this.platformId) &&
|
||||||
|
!this.transferState.hasKey(transferStateKey)
|
||||||
|
) {
|
||||||
|
this.transferState.set(transferStateKey, translation);
|
||||||
|
}
|
||||||
|
return translation;
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
lang === FALLBACK_LANG
|
||||||
|
? Promise.resolve({})
|
||||||
|
: this.loadTranslation(FALLBACK_LANG),
|
||||||
|
);
|
||||||
|
|
||||||
|
translationCache.set(lang, pending);
|
||||||
|
return pending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, DestroyRef, computed, inject, signal } from '@angular/core';
|
import {
|
||||||
|
afterNextRender,
|
||||||
|
Component,
|
||||||
|
DestroyRef,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import {
|
import {
|
||||||
NavigationStart,
|
NavigationStart,
|
||||||
@@ -58,16 +65,9 @@ export class NavbarComponent {
|
|||||||
];
|
];
|
||||||
|
|
||||||
constructor(public langService: LanguageService) {
|
constructor(public langService: LanguageService) {
|
||||||
if (!this.shopService.cartLoaded()) {
|
afterNextRender(() => {
|
||||||
this.shopService
|
this.scheduleCartWarmup();
|
||||||
.loadCart()
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe({
|
|
||||||
error: () => {
|
|
||||||
this.shopService.cart.set(null);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
@@ -96,6 +96,9 @@ export class NavbarComponent {
|
|||||||
toggleCart(): void {
|
toggleCart(): void {
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
this.isCartOpen.update((open) => !open);
|
this.isCartOpen.update((open) => !open);
|
||||||
|
if (this.isCartOpen()) {
|
||||||
|
this.loadCartIfNeeded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeCart(): void {
|
closeCart(): void {
|
||||||
@@ -192,5 +195,44 @@ export class NavbarComponent {
|
|||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private scheduleCartWarmup(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
this.loadCartIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warmup = () => this.loadCartIfNeeded();
|
||||||
|
const idleCallback = (
|
||||||
|
window as Window & {
|
||||||
|
requestIdleCallback?: (
|
||||||
|
callback: IdleRequestCallback,
|
||||||
|
options?: IdleRequestOptions,
|
||||||
|
) => number;
|
||||||
|
}
|
||||||
|
).requestIdleCallback;
|
||||||
|
|
||||||
|
if (typeof idleCallback === 'function') {
|
||||||
|
idleCallback(() => warmup(), { timeout: 1500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(warmup, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadCartIfNeeded(): void {
|
||||||
|
if (this.shopService.cartLoaded() || this.shopService.cartLoading()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shopService
|
||||||
|
.loadCart()
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe({
|
||||||
|
error: () => {
|
||||||
|
this.shopService.cart.set(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly routes = routes;
|
protected readonly routes = routes;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
|
afterNextRender,
|
||||||
Component,
|
Component,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
Injector,
|
Injector,
|
||||||
@@ -193,16 +194,9 @@ export class ProductDetailComponent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!this.shopService.cartLoaded()) {
|
afterNextRender(() => {
|
||||||
this.shopService
|
this.scheduleCartWarmup();
|
||||||
.loadCart()
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe({
|
|
||||||
error: () => {
|
|
||||||
this.shopService.cart.set(null);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
combineLatest([
|
combineLatest([
|
||||||
toObservable(this.productSlug, { injector: this.injector }),
|
toObservable(this.productSlug, { injector: this.injector }),
|
||||||
@@ -282,6 +276,45 @@ export class ProductDetailComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private scheduleCartWarmup(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
this.loadCartIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warmup = () => this.loadCartIfNeeded();
|
||||||
|
const idleCallback = (
|
||||||
|
window as Window & {
|
||||||
|
requestIdleCallback?: (
|
||||||
|
callback: IdleRequestCallback,
|
||||||
|
options?: IdleRequestOptions,
|
||||||
|
) => number;
|
||||||
|
}
|
||||||
|
).requestIdleCallback;
|
||||||
|
|
||||||
|
if (typeof idleCallback === 'function') {
|
||||||
|
idleCallback(() => warmup(), { timeout: 1500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(warmup, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadCartIfNeeded(): void {
|
||||||
|
if (this.shopService.cartLoaded() || this.shopService.cartLoading()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shopService
|
||||||
|
.loadCart()
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe({
|
||||||
|
error: () => {
|
||||||
|
this.shopService.cart.set(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
selectImage(mediaAssetId: string): void {
|
selectImage(mediaAssetId: string): void {
|
||||||
this.setSelectedImageAssetId(mediaAssetId);
|
this.setSelectedImageAssetId(mediaAssetId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
|
afterNextRender,
|
||||||
Component,
|
Component,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
Injector,
|
Injector,
|
||||||
@@ -89,16 +90,9 @@ export class ShopPageComponent {
|
|||||||
readonly cartHasItems = computed(() => this.cartItems().length > 0);
|
readonly cartHasItems = computed(() => this.cartItems().length > 0);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!this.shopService.cartLoaded()) {
|
afterNextRender(() => {
|
||||||
this.shopService
|
this.scheduleCartWarmup();
|
||||||
.loadCart()
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe({
|
|
||||||
error: () => {
|
|
||||||
this.shopService.cart.set(null);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
combineLatest([
|
combineLatest([
|
||||||
toObservable(this.categorySlug, { injector: this.injector }),
|
toObservable(this.categorySlug, { injector: this.injector }),
|
||||||
@@ -150,6 +144,45 @@ export class ShopPageComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private scheduleCartWarmup(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
this.loadCartIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warmup = () => this.loadCartIfNeeded();
|
||||||
|
const idleCallback = (
|
||||||
|
window as Window & {
|
||||||
|
requestIdleCallback?: (
|
||||||
|
callback: IdleRequestCallback,
|
||||||
|
options?: IdleRequestOptions,
|
||||||
|
) => number;
|
||||||
|
}
|
||||||
|
).requestIdleCallback;
|
||||||
|
|
||||||
|
if (typeof idleCallback === 'function') {
|
||||||
|
idleCallback(() => warmup(), { timeout: 1500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(warmup, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadCartIfNeeded(): void {
|
||||||
|
if (this.shopService.cartLoaded() || this.shopService.cartLoading()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shopService
|
||||||
|
.loadCart()
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe({
|
||||||
|
error: () => {
|
||||||
|
this.shopService.cart.set(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
productCartQuantity(productId: string): number {
|
productCartQuantity(productId: string): number {
|
||||||
return this.shopService.quantityForProduct(productId);
|
return this.shopService.quantityForProduct(productId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,32 @@
|
|||||||
<base href="/" />
|
<base href="/" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/png" href="assets/images/Fav-icon.png" />
|
<link rel="icon" type="image/png" href="assets/images/Fav-icon.png" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
/>
|
||||||
|
<noscript>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
</noscript>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||||
|
rel="stylesheet"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
/>
|
||||||
|
<noscript>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
</noscript>
|
||||||
<meta name="msvalidate.01" content="5AF60C1471E1800B39DF4DBC3C709035" />
|
<meta name="msvalidate.01" content="5AF60C1471E1800B39DF4DBC3C709035" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user