feat(back-end front-end): shop route
This commit is contained in:
101
frontend/src/app/features/shop/services/shop-route.service.ts
Normal file
101
frontend/src/app/features/shop/services/shop-route.service.ts
Normal file
@@ -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] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user