Merge remote-tracking branch 'origin/feat/brand-logo' into feat/brand-logo
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
|
||||||
@if (siteIntroState() !== 'hidden') {
|
@if (siteIntroState() !== "hidden") {
|
||||||
<div
|
<div
|
||||||
class="site-intro"
|
class="site-intro"
|
||||||
[class.site-intro--closing]="siteIntroState() === 'closing'"
|
[class.site-intro--closing]="siteIntroState() === 'closing'"
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ export function parseAcceptLanguage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const qualityParam = params.find((param) => param.startsWith('q='));
|
const qualityParam = params.find((param) => param.startsWith('q='));
|
||||||
const quality = qualityParam ? Number.parseFloat(qualityParam.slice(2)) : 1;
|
const quality = qualityParam
|
||||||
|
? Number.parseFloat(qualityParam.slice(2))
|
||||||
|
: 1;
|
||||||
return {
|
return {
|
||||||
tag: rawTag,
|
tag: rawTag,
|
||||||
quality: Number.isFinite(quality) ? quality : 0,
|
quality: Number.isFinite(quality) ? quality : 0,
|
||||||
@@ -70,7 +72,9 @@ export function parseAcceptLanguage(
|
|||||||
index: number;
|
index: number;
|
||||||
} => entry !== null && entry.quality > 0,
|
} => entry !== null && entry.quality > 0,
|
||||||
)
|
)
|
||||||
.sort((left, right) => right.quality - left.quality || left.index - right.index)
|
.sort(
|
||||||
|
(left, right) => right.quality - left.quality || left.index - right.index,
|
||||||
|
)
|
||||||
.map((entry) => entry.tag);
|
.map((entry) => entry.tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +106,7 @@ function resolveExplicitLanguageFromUrl(
|
|||||||
const normalizedUrl = String(url ?? '/');
|
const normalizedUrl = String(url ?? '/');
|
||||||
const [pathAndQuery] = normalizedUrl.split('#', 1);
|
const [pathAndQuery] = normalizedUrl.split('#', 1);
|
||||||
const [rawPath, rawQuery] = pathAndQuery.split('?', 2);
|
const [rawPath, rawQuery] = pathAndQuery.split('?', 2);
|
||||||
const firstSegment = rawPath
|
const firstSegment = rawPath.split('/').filter(Boolean)[0];
|
||||||
.split('/')
|
|
||||||
.filter(Boolean)[0];
|
|
||||||
const pathLanguage = normalizeSupportedLanguage(firstSegment);
|
const pathLanguage = normalizeSupportedLanguage(firstSegment);
|
||||||
if (pathLanguage) {
|
if (pathLanguage) {
|
||||||
return pathLanguage;
|
return pathLanguage;
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ type SupportedLang = 'it' | 'en' | 'de' | 'fr';
|
|||||||
const FALLBACK_LANG: SupportedLang = 'it';
|
const FALLBACK_LANG: SupportedLang = 'it';
|
||||||
const translationCache = new Map<SupportedLang, Promise<TranslationObject>>();
|
const translationCache = new Map<SupportedLang, Promise<TranslationObject>>();
|
||||||
|
|
||||||
const translationLoaders: Record<SupportedLang, () => Promise<TranslationObject>> = {
|
const translationLoaders: Record<
|
||||||
|
SupportedLang,
|
||||||
|
() => Promise<TranslationObject>
|
||||||
|
> = {
|
||||||
it: () =>
|
it: () =>
|
||||||
import('../../../assets/i18n/it.json').then(
|
import('../../../assets/i18n/it.json').then(
|
||||||
(module) => module.default as TranslationObject,
|
(module) => module.default as TranslationObject,
|
||||||
@@ -51,8 +54,9 @@ export class StaticTranslateLoader implements TranslateLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private loadTranslation(lang: SupportedLang): Promise<TranslationObject> {
|
private loadTranslation(lang: SupportedLang): Promise<TranslationObject> {
|
||||||
const transferStateKey =
|
const transferStateKey = makeStateKey<TranslationObject>(
|
||||||
makeStateKey<TranslationObject>(`i18n:${lang.toLowerCase()}`);
|
`i18n:${lang.toLowerCase()}`,
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
isPlatformBrowser(this.platformId) &&
|
isPlatformBrowser(this.platformId) &&
|
||||||
this.transferState.hasKey(transferStateKey)
|
this.transferState.hasKey(transferStateKey)
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ import { FooterComponent } from './footer.component';
|
|||||||
templateUrl: './layout.component.html',
|
templateUrl: './layout.component.html',
|
||||||
styleUrl: './layout.component.scss',
|
styleUrl: './layout.component.scss',
|
||||||
})
|
})
|
||||||
export class LayoutComponent {
|
export class LayoutComponent {}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
import { CommonModule, NgOptimizedImage } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
afterNextRender,
|
afterNextRender,
|
||||||
Component,
|
Component,
|
||||||
@@ -30,7 +30,13 @@ import {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterLink, RouterLinkActive, TranslateModule, NgOptimizedImage],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterLink,
|
||||||
|
RouterLinkActive,
|
||||||
|
TranslateModule,
|
||||||
|
NgOptimizedImage,
|
||||||
|
],
|
||||||
templateUrl: './navbar.component.html',
|
templateUrl: './navbar.component.html',
|
||||||
styleUrls: ['./navbar.component.scss'],
|
styleUrls: ['./navbar.component.scss'],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ export class LanguageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentTree = this.router.parseUrl(this.router.url);
|
const currentTree = this.router.parseUrl(this.router.url);
|
||||||
const localizedRoute = this.resolveLocalizedRouteOverride(currentTree, lang);
|
const localizedRoute = this.resolveLocalizedRouteOverride(
|
||||||
|
currentTree,
|
||||||
|
lang,
|
||||||
|
);
|
||||||
if (localizedRoute) {
|
if (localizedRoute) {
|
||||||
this.navigateToLocalizedRoute(currentTree, localizedRoute);
|
this.navigateToLocalizedRoute(currentTree, localizedRoute);
|
||||||
return;
|
return;
|
||||||
@@ -220,7 +223,9 @@ export class LanguageService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPath = this.getCleanPath(this.router.serializeUrl(currentTree));
|
const currentPath = this.getCleanPath(
|
||||||
|
this.router.serializeUrl(currentTree),
|
||||||
|
);
|
||||||
const paths = Object.values(overrides)
|
const paths = Object.values(overrides)
|
||||||
.map((path) => this.normalizeLocalizedRoutePath(path))
|
.map((path) => this.normalizeLocalizedRoutePath(path))
|
||||||
.filter((path): path is string => !!path);
|
.filter((path): path is string => !!path);
|
||||||
|
|||||||
@@ -360,18 +360,23 @@ export class SeoService {
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeAlternatePaths(paths: SeoMap | null | undefined): SeoMap | null {
|
private normalizeAlternatePaths(
|
||||||
|
paths: SeoMap | null | undefined,
|
||||||
|
): SeoMap | null {
|
||||||
if (!paths) {
|
if (!paths) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = this.supportedLangs.reduce<SeoMap>((accumulator, lang) => {
|
const normalized = this.supportedLangs.reduce<SeoMap>(
|
||||||
const path = this.normalizeSeoPath(paths[lang]);
|
(accumulator, lang) => {
|
||||||
if (path) {
|
const path = this.normalizeSeoPath(paths[lang]);
|
||||||
accumulator[lang] = path;
|
if (path) {
|
||||||
}
|
accumulator[lang] = path;
|
||||||
return accumulator;
|
}
|
||||||
}, {});
|
return accumulator;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
return Object.keys(normalized).length > 0 ? normalized : null;
|
return Object.keys(normalized).length > 0 ? normalized : null;
|
||||||
}
|
}
|
||||||
@@ -445,7 +450,10 @@ export class SeoService {
|
|||||||
if (!path) {
|
if (!path) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.appendAlternateLink(this.seoLocaleByLang[alt], this.toAbsoluteUrl(path));
|
this.appendAlternateLink(
|
||||||
|
this.seoLocaleByLang[alt],
|
||||||
|
this.toAbsoluteUrl(path),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (xDefaultPath) {
|
if (xDefaultPath) {
|
||||||
this.appendAlternateLink('x-default', this.toAbsoluteUrl(xDefaultPath));
|
this.appendAlternateLink('x-default', this.toAbsoluteUrl(xDefaultPath));
|
||||||
|
|||||||
@@ -18,10 +18,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="animation-stage" [attr.data-variant]="variant()">
|
||||||
class="animation-stage"
|
|
||||||
[attr.data-variant]="variant()"
|
|
||||||
>
|
|
||||||
<app-brand-animation-logo
|
<app-brand-animation-logo
|
||||||
[variant]="variant()"
|
[variant]="variant()"
|
||||||
[decorative]="false"
|
[decorative]="false"
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ export class ProductDetailComponent {
|
|||||||
this.shopService.resolveMediaUrl(image.hero) ??
|
this.shopService.resolveMediaUrl(image.hero) ??
|
||||||
this.shopService.resolveMediaUrl(image.card) ??
|
this.shopService.resolveMediaUrl(image.card) ??
|
||||||
this.shopService.resolveMediaUrl(image.thumb)
|
this.shopService.resolveMediaUrl(image.thumb)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleCartWarmup(): void {
|
private scheduleCartWarmup(): void {
|
||||||
@@ -774,9 +774,12 @@ export class ProductDetailComponent {
|
|||||||
const targetPath =
|
const targetPath =
|
||||||
product.localizedPaths?.[lang] ??
|
product.localizedPaths?.[lang] ??
|
||||||
`/${lang}/shop/p/${this.shopRouteService.productPathSegment(product)}`;
|
`/${lang}/shop/p/${this.shopRouteService.productPathSegment(product)}`;
|
||||||
const normalizedTargetPath =
|
const normalizedTargetPath = targetPath.startsWith('/')
|
||||||
targetPath.startsWith('/') ? targetPath : `/${targetPath}`;
|
? targetPath
|
||||||
const currentPath = this.router.serializeUrl(currentTree).split(/[?#]/, 1)[0];
|
: `/${targetPath}`;
|
||||||
|
const currentPath = this.router
|
||||||
|
.serializeUrl(currentTree)
|
||||||
|
.split(/[?#]/, 1)[0];
|
||||||
if (currentPath === normalizedTargetPath) {
|
if (currentPath === normalizedTargetPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,9 @@ describe('ShopService', () => {
|
|||||||
});
|
});
|
||||||
catalogRequest.flush(buildCatalog());
|
catalogRequest.flush(buildCatalog());
|
||||||
|
|
||||||
httpMock.expectNone('http://localhost:8000/api/shop/products/desk-cable-clip');
|
httpMock.expectNone(
|
||||||
|
'http://localhost:8000/api/shop/products/desk-cable-clip',
|
||||||
|
);
|
||||||
expect(errorResponse?.status).toBe(404);
|
expect(errorResponse?.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,7 +293,9 @@ describe('ShopService', () => {
|
|||||||
});
|
});
|
||||||
catalogRequest.flush(buildCatalog());
|
catalogRequest.flush(buildCatalog());
|
||||||
|
|
||||||
httpMock.expectNone('http://localhost:8000/api/shop/products/desk-cable-clip');
|
httpMock.expectNone(
|
||||||
|
'http://localhost:8000/api/shop/products/desk-cable-clip',
|
||||||
|
);
|
||||||
expect(errorResponse?.status).toBe(404);
|
expect(errorResponse?.status).toBe(404);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -292,8 +292,9 @@ export class ShopService {
|
|||||||
|
|
||||||
return this.getProductCatalog().pipe(
|
return this.getProductCatalog().pipe(
|
||||||
map((catalog) =>
|
map((catalog) =>
|
||||||
catalog.products.find((product) =>
|
catalog.products.find(
|
||||||
this.normalizePublicPath(product.publicPath) === normalizedPath,
|
(product) =>
|
||||||
|
this.normalizePublicPath(product.publicPath) === normalizedPath,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
switchMap((product) => {
|
switchMap((product) => {
|
||||||
|
|||||||
@@ -29,14 +29,14 @@
|
|||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-animation[data-variant='site-intro'] .brand-animation__letter {
|
.brand-animation[data-variant="site-intro"] .brand-animation__letter {
|
||||||
animation: site-intro-preview var(--brand-animation-site-intro-duration, 1s) linear 1 forwards;
|
animation: site-intro-preview var(--brand-animation-site-intro-duration, 1s)
|
||||||
|
linear 1 forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-animation[data-variant='calculator-loader'] .brand-animation__letter {
|
.brand-animation[data-variant="calculator-loader"] .brand-animation__letter {
|
||||||
animation: calculator-loader-loop
|
animation: calculator-loader-loop
|
||||||
var(--brand-animation-loader-loop-duration, 2.65s)
|
var(--brand-animation-loader-loop-duration, 2.65s) infinite;
|
||||||
infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes site-intro-preview {
|
@keyframes site-intro-preview {
|
||||||
@@ -75,8 +75,7 @@
|
|||||||
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
|
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
scaleX(var(--loader-group-scale-x))
|
scaleX(var(--loader-group-scale-x)) scaleY(var(--loader-group-scale-y));
|
||||||
scaleY(var(--loader-group-scale-y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
12% {
|
12% {
|
||||||
@@ -87,8 +86,7 @@
|
|||||||
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
|
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
scaleX(var(--loader-group-scale-x))
|
scaleX(var(--loader-group-scale-x)) scaleY(var(--loader-group-scale-y));
|
||||||
scaleY(var(--loader-group-scale-y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
12% {
|
12% {
|
||||||
@@ -118,8 +116,7 @@
|
|||||||
var(--bee-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
|
var(--bee-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
scaleX(var(--loader-group-scale-x))
|
scaleX(var(--loader-group-scale-x)) scaleY(var(--loader-group-scale-y));
|
||||||
scaleY(var(--loader-group-scale-y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
88% {
|
88% {
|
||||||
@@ -131,12 +128,11 @@
|
|||||||
transform: translate(-50%, -50%)
|
transform: translate(-50%, -50%)
|
||||||
translateX(
|
translateX(
|
||||||
calc(
|
calc(
|
||||||
(var(--bee-anchor-x) + var(--loader-exit-shift)) *
|
(var(--bee-anchor-x) + var(--loader-exit-shift)) * var(--word-scale) *
|
||||||
var(--word-scale) * var(--word-spacing-factor)
|
var(--word-spacing-factor)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
scaleX(0.98)
|
scaleX(0.98) scaleY(1.02);
|
||||||
scaleY(1.02);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
94.01%,
|
94.01%,
|
||||||
@@ -148,8 +144,7 @@
|
|||||||
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
|
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
scaleX(var(--loader-group-scale-x))
|
scaleX(var(--loader-group-scale-x)) scaleY(var(--loader-group-scale-y));
|
||||||
scaleY(var(--loader-group-scale-y));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ export class BrandAnimationLogoComponent {
|
|||||||
readonly resolvedLetters = computed<ResolvedAnimationLetter[]>(() =>
|
readonly resolvedLetters = computed<ResolvedAnimationLetter[]>(() =>
|
||||||
LETTERS.map((letter) => ({
|
LETTERS.map((letter) => ({
|
||||||
key: letter.key,
|
key: letter.key,
|
||||||
src:
|
src: this.variant() === 'site-intro' ? letter.yellowSrc : letter.darkSrc,
|
||||||
this.variant() === 'site-intro' ? letter.yellowSrc : letter.darkSrc,
|
|
||||||
wordX: letter.wordX,
|
wordX: letter.wordX,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ describe('server routing redirects', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('redirects legacy shop product aliases to the canonical product route', () => {
|
it('redirects legacy shop product aliases to the canonical product route', () => {
|
||||||
expect(resolvePublicRedirectTarget('/shop/accessories/desk-cable-clip')).toBe(
|
expect(
|
||||||
'/it/shop/p/desk-cable-clip',
|
resolvePublicRedirectTarget('/shop/accessories/desk-cable-clip'),
|
||||||
);
|
).toBe('/it/shop/p/desk-cable-clip');
|
||||||
expect(
|
expect(
|
||||||
resolvePublicRedirectTarget('/de/shop/zubehor/schreibtisch-kabelhalter'),
|
resolvePublicRedirectTarget('/de/shop/zubehor/schreibtisch-kabelhalter'),
|
||||||
).toBe('/de/shop/p/schreibtisch-kabelhalter');
|
).toBe('/de/shop/p/schreibtisch-kabelhalter');
|
||||||
|
|||||||
@@ -99,5 +99,7 @@ function splitSegments(pathname: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function looksLikeLangToken(segment: string | null | undefined): boolean {
|
function looksLikeLangToken(segment: string | null | undefined): boolean {
|
||||||
return typeof segment === 'string' && /^[a-z]{2}(?:-[a-z]{2})?$/i.test(segment);
|
return (
|
||||||
|
typeof segment === 'string' && /^[a-z]{2}(?:-[a-z]{2})?$/i.test(segment)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user