From 126d3ef4c400bc744416f50493bb8b14af41553e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Tue, 10 Mar 2026 15:04:56 +0100 Subject: [PATCH] feat(back-end front-end): shop route --- .../shop/services/shop-route.service.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 frontend/src/app/features/shop/services/shop-route.service.ts diff --git a/frontend/src/app/features/shop/services/shop-route.service.ts b/frontend/src/app/features/shop/services/shop-route.service.ts new file mode 100644 index 0000000..408747a --- /dev/null +++ b/frontend/src/app/features/shop/services/shop-route.service.ts @@ -0,0 +1,101 @@ +import { Injectable } from '@angular/core'; +import { LanguageService } from '../../../core/services/language.service'; + +export interface ShopProductRouteRef { + id: string | null | undefined; + name: string | null | undefined; + slug?: string | null | undefined; +} + +export interface ShopProductLookup { + idPrefix: string | null; + slugHint: string | null; +} + +@Injectable({ + providedIn: 'root', +}) +export class ShopRouteService { + constructor(private readonly languageService: LanguageService) {} + + shopRootCommands(categorySlug?: string | null): string[] { + const lang = this.languageService.currentLang(); + return categorySlug + ? ['/', lang, 'shop', categorySlug] + : ['/', lang, 'shop']; + } + + productCommands(product: ShopProductRouteRef): string[] { + const lang = this.languageService.currentLang(); + return ['/', lang, 'shop', 'p', this.productPathSegment(product)]; + } + + productPathSegment(product: ShopProductRouteRef): string { + const idPrefix = this.productIdPrefix(product.id); + const tail = + this.slugify(product.name) || this.slugify(product.slug) || 'product'; + + return idPrefix ? `${idPrefix}-${tail}` : tail; + } + + resolveProductLookup(productPathSegment: string | null | undefined): ShopProductLookup { + const normalized = String(productPathSegment ?? '').trim().toLowerCase(); + if (!normalized) { + return { + idPrefix: null, + slugHint: null, + }; + } + + const bareUuidMatch = normalized.match(/^([0-9a-f]{8})$/); + if (bareUuidMatch) { + return { + idPrefix: bareUuidMatch[1], + slugHint: null, + }; + } + + const publicSlugMatch = normalized.match(/^([0-9a-f]{8})-(.+)$/); + if (publicSlugMatch) { + return { + idPrefix: publicSlugMatch[1], + slugHint: this.slugify(publicSlugMatch[2]) || null, + }; + } + + return { + idPrefix: null, + slugHint: normalized, + }; + } + + isCatalogUrl(url: string | null | undefined): boolean { + if (!url) { + return false; + } + + const normalized = url.split(/[?#]/, 1)[0] || ''; + return /^\/(?:[a-z]{2}\/)?shop(?:\/[^/]+)?$/i.test(normalized); + } + + slugify(value: string | null | undefined): string { + return String(value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-{2,}/g, '-'); + } + + private productIdPrefix(productId: string | null | undefined): string { + const normalized = String(productId ?? '').trim().toLowerCase(); + const canonicalUuidMatch = normalized.match(/^([0-9a-f]{8})-/); + if (canonicalUuidMatch) { + return canonicalUuidMatch[1]; + } + + const compactUuidMatch = normalized.match(/^([0-9a-f]{8})/); + return compactUuidMatch?.[1] ?? ''; + } +}