diff --git a/.gitignore b/.gitignore index 3d84327..1e548f4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ appsettings.*.local.json # OS Thumbs.db .DS_Store + +.unlighthouse/ diff --git a/docker/docker-compose.local.yml b/docker/docker-compose.local.yml new file mode 100644 index 0000000..2a1273c --- /dev/null +++ b/docker/docker-compose.local.yml @@ -0,0 +1,18 @@ +# Local-only port publishes. Use with: +# docker compose -f docker-compose.yml -f docker-compose.local.yml up -d postgres redis openobserve +services: + postgres: + ports: + - "5433:5432" + redis: + ports: + - "6379:6379" + openobserve: + ports: + - "5080:5080" + +networks: + iled: + external: true + homelab: + external: true diff --git a/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.cs b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.cs index ea103d7..7186a6e 100644 --- a/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.cs +++ b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/shop/src/app/features/cart/cart.component.html b/src/shop/src/app/features/cart/cart.component.html index 04b0fac..adb1fe5 100644 --- a/src/shop/src/app/features/cart/cart.component.html +++ b/src/shop/src/app/features/cart/cart.component.html @@ -53,8 +53,11 @@ } @else { iLED diff --git a/src/shop/src/app/features/home/home.component.html b/src/shop/src/app/features/home/home.component.html index 61e3db1..202b793 100644 --- a/src/shop/src/app/features/home/home.component.html +++ b/src/shop/src/app/features/home/home.component.html @@ -57,8 +57,11 @@

{{ 'common.loading' | translate }}

} @else {
- @for (product of bestsellers(); track product.id) { - + @for (product of bestsellers(); track product.id; let i = $index) { + }
} diff --git a/src/shop/src/app/features/products/product-detail.component.html b/src/shop/src/app/features/products/product-detail.component.html index efbf413..d6146bf 100644 --- a/src/shop/src/app/features/products/product-detail.component.html +++ b/src/shop/src/app/features/products/product-detail.component.html @@ -31,6 +31,10 @@ @@ -40,13 +44,17 @@ -
+
@if (activeImage()) { } @else {
- @for (product of products(); track product.id) { - + @for (product of products(); track product.id; let i = $index) { + }
} diff --git a/src/shop/src/app/shared/layout/footer.component.html b/src/shop/src/app/shared/layout/footer.component.html index 86c1a9f..617771e 100644 --- a/src/shop/src/app/shared/layout/footer.component.html +++ b/src/shop/src/app/shared/layout/footer.component.html @@ -7,56 +7,56 @@ iLED
-

+

{{ 'footer.tagline' | translate }}

-

+

{{ 'footer.secureCheckout' | translate }}

-

+

{{ 'footer.customerService' | translate }} -

- +

+
{{ 'footer.shipping' | translate }} - + {{ 'footer.returnsPolicy' | translate }} - + {{ 'footer.contact' | translate }}
-

+

{{ 'footer.about' | translate }} -

- +

+
{{ 'footer.aboutUs' | translate }} - + {{ 'footer.privacy' | translate }} - + {{ 'footer.terms' | translate }} - + {{ 'footer.cookies' | translate }}
-

+

{{ 'footer.advice' | translate }} -

- +

+
{{ 'footer.faq' | translate }}
{{ 'footer.copyright' | translate }}
diff --git a/src/shop/src/app/shared/layout/layout.components.ts b/src/shop/src/app/shared/layout/layout.components.ts index c6d6a36..8efbded 100644 --- a/src/shop/src/app/shared/layout/layout.components.ts +++ b/src/shop/src/app/shared/layout/layout.components.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, + computed, DestroyRef, effect, HostListener, @@ -36,29 +37,35 @@ import { import { ProductListItem, PromoBanner } from '../../core/models'; import { CartAddedModalComponent } from '../cart-added-modal.component'; +const PROMO_DISMISSED_KEY = 'iled_promo_dismissed'; +const PROMO_EXPECT_KEY = 'iled_promo_expect'; + @Component({ selector: 'app-promo-banner', standalone: true, imports: [TranslatePipe], template: ` - @if (banner() && !dismissed()) { + @if (showSlot()) {
- {{ banner()!.text }} - @if (banner()!.highlightedCode) { - {{ banner()!.highlightedCode }} + @if (banner(); as b) { + {{ b.text }} + @if (b.highlightedCode) { + {{ b.highlightedCode }} + } + } -
} `, @@ -68,21 +75,46 @@ export class PromoBannerComponent { readonly #catalog = inject(CatalogService); readonly #i18n = inject(I18nService); readonly banner = signal(null); + readonly loaded = signal(false); readonly dismissed = signal( - sessionStorage.getItem('iled_promo_dismissed') === '1', + sessionStorage.getItem(PROMO_DISMISSED_KEY) === '1', ); + /** + * Reserve banner height until the API responds (or forever while visible) + * so sticky header / page content do not jump. + */ + readonly showSlot = computed(() => { + if (this.dismissed()) { + return false; + } + if (!this.loaded()) { + return sessionStorage.getItem(PROMO_EXPECT_KEY) !== '0'; + } + return this.banner() !== null; + }); + constructor() { effect(() => { this.#i18n.locale(); + this.loaded.set(false); this.#catalog.getBanner().subscribe({ - next: (b) => this.banner.set(b), + next: (b) => { + this.banner.set(b); + this.loaded.set(true); + sessionStorage.setItem(PROMO_EXPECT_KEY, b ? '1' : '0'); + }, + error: () => { + this.banner.set(null); + this.loaded.set(true); + sessionStorage.setItem(PROMO_EXPECT_KEY, '0'); + }, }); }); } dismiss(): void { - sessionStorage.setItem('iled_promo_dismissed', '1'); + sessionStorage.setItem(PROMO_DISMISSED_KEY, '1'); this.dismissed.set(true); } } @@ -230,6 +262,8 @@ export class StarRatingComponent { }) export class ProductCardComponent implements OnInit { readonly product = input.required(); + /** Eager-load + high fetch priority for above-the-fold LCP cards. */ + readonly priority = input(false); readonly #cart = inject(CartService); readonly #catalog = inject(CatalogService); readonly #favorites = inject(FavoritesService); @@ -328,8 +362,10 @@ export class ProductCardComponent implements OnInit { {{ 'a11y.skipToContent' | translate }}
- - +
+ + +
@@ -44,9 +47,9 @@
-

+

{{ product().name }} -

+

diff --git a/src/shop/src/styles.css b/src/shop/src/styles.css index ea4a5c1..54023b8 100644 --- a/src/shop/src/styles.css +++ b/src/shop/src/styles.css @@ -10,7 +10,8 @@ --color-iled-grey-light: #d9d9d9; --color-iled-bg: #fafaf8; --color-iled-surface: #ffffff; - --color-iled-success: #16a34a; + /* Darker green for AA contrast on white at small sizes */ + --color-iled-success: #15803d; --color-iled-danger: #dc2626; --color-iled-focus: #ffbc39; --font-sans: 'Outfit', ui-sans-serif, system-ui, sans-serif;