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 { Observable, of } 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';
|
||||
import { from, Observable } from 'rxjs';
|
||||
|
||||
const TRANSLATIONS: Record<string, TranslationObject> = {
|
||||
it: it as TranslationObject,
|
||||
en: en as TranslationObject,
|
||||
de: de as TranslationObject,
|
||||
fr: fr as TranslationObject,
|
||||
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
|
||||
|
||||
const FALLBACK_LANG: SupportedLang = 'it';
|
||||
const translationCache = new Map<SupportedLang, Promise<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()
|
||||
export class StaticTranslateLoader implements TranslateLoader {
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
private readonly transferState = inject(TransferState);
|
||||
|
||||
getTranslation(lang: string): Observable<TranslationObject> {
|
||||
const normalized = String(lang || 'it').toLowerCase();
|
||||
return of(TRANSLATIONS[normalized] ?? TRANSLATIONS['it']);
|
||||
const normalized = this.normalizeLanguage(lang);
|
||||
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 { 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 {
|
||||
NavigationStart,
|
||||
@@ -58,16 +65,9 @@ export class NavbarComponent {
|
||||
];
|
||||
|
||||
constructor(public langService: LanguageService) {
|
||||
if (!this.shopService.cartLoaded()) {
|
||||
this.shopService
|
||||
.loadCart()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
error: () => {
|
||||
this.shopService.cart.set(null);
|
||||
},
|
||||
afterNextRender(() => {
|
||||
this.scheduleCartWarmup();
|
||||
});
|
||||
}
|
||||
|
||||
this.router.events
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
@@ -96,6 +96,9 @@ export class NavbarComponent {
|
||||
toggleCart(): void {
|
||||
this.closeMenu();
|
||||
this.isCartOpen.update((open) => !open);
|
||||
if (this.isCartOpen()) {
|
||||
this.loadCartIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
closeCart(): void {
|
||||
@@ -192,5 +195,44 @@ export class NavbarComponent {
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
import {
|
||||
afterNextRender,
|
||||
Component,
|
||||
DestroyRef,
|
||||
Injector,
|
||||
@@ -193,16 +194,9 @@ export class ProductDetailComponent {
|
||||
);
|
||||
|
||||
constructor() {
|
||||
if (!this.shopService.cartLoaded()) {
|
||||
this.shopService
|
||||
.loadCart()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
error: () => {
|
||||
this.shopService.cart.set(null);
|
||||
},
|
||||
afterNextRender(() => {
|
||||
this.scheduleCartWarmup();
|
||||
});
|
||||
}
|
||||
|
||||
combineLatest([
|
||||
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 {
|
||||
this.setSelectedImageAssetId(mediaAssetId);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
afterNextRender,
|
||||
Component,
|
||||
DestroyRef,
|
||||
Injector,
|
||||
@@ -89,16 +90,9 @@ export class ShopPageComponent {
|
||||
readonly cartHasItems = computed(() => this.cartItems().length > 0);
|
||||
|
||||
constructor() {
|
||||
if (!this.shopService.cartLoaded()) {
|
||||
this.shopService
|
||||
.loadCart()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
error: () => {
|
||||
this.shopService.cart.set(null);
|
||||
},
|
||||
afterNextRender(() => {
|
||||
this.scheduleCartWarmup();
|
||||
});
|
||||
}
|
||||
|
||||
combineLatest([
|
||||
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 {
|
||||
return this.shopService.quantityForProduct(productId);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,32 @@
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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
|
||||
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"
|
||||
/>
|
||||
</noscript>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</noscript>
|
||||
<meta name="msvalidate.01" content="5AF60C1471E1800B39DF4DBC3C709035" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user