diff --git a/app/[locale]/auth/reset-password/page.tsx b/app/[locale]/auth/reset-password/page.tsx index 13921c16..82796180 100644 --- a/app/[locale]/auth/reset-password/page.tsx +++ b/app/[locale]/auth/reset-password/page.tsx @@ -25,6 +25,18 @@ export default function ResetPasswordPage() { const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); + if (!token) { + return ( + +

{dict.auth.resetPasswordTitle}

+ + + {dict.auth.requestNewLink} + +
+ ); + } + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -61,18 +73,6 @@ export default function ResetPasswordPage() { } }; - if (!token) { - return ( - -

{dict.auth.resetPasswordTitle}

- - - {dict.auth.requestNewLink} - -
- ); - } - return (

{dict.auth.resetPasswordTitle}

diff --git a/app/[locale]/cart/design-preview.tsx b/app/[locale]/cart/design-preview.tsx index 3bc53c6e..33876fe7 100644 --- a/app/[locale]/cart/design-preview.tsx +++ b/app/[locale]/cart/design-preview.tsx @@ -87,8 +87,8 @@ function loadImage(src: string): Promise { return new Promise((resolve) => { const img = new Image(); img.crossOrigin = 'anonymous'; - img.onload = () => resolve(img); - img.onerror = () => resolve(null); + img.addEventListener('load', () => resolve(img)); + img.addEventListener('error', () => resolve(null)); img.src = src; }); } diff --git a/app/[locale]/cart/page.tsx b/app/[locale]/cart/page.tsx index 7b103d2a..441893f5 100644 --- a/app/[locale]/cart/page.tsx +++ b/app/[locale]/cart/page.tsx @@ -45,9 +45,9 @@ export default async function CartPage({ productIds.map((id) => productRepository.findById(id, locale)), ); const productMap = new Map(); - products.forEach((p) => { + for (const p of products) { if (p) productMap.set(p.id, p); - }); + } items = cart.items.map((item) => { const product = productMap.get(item.productId.value); diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 4788e69e..52abbba2 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -42,9 +42,9 @@ export async function generateMetadata({ const baseUrl = APP_BASE_URL; const alternates: Record = {}; - ['es', 'cat'].forEach((lang) => { + for (const lang of ['es', 'cat']) { alternates[lang] = `${baseUrl}/${lang}`; - }); + } return { metadataBase: new URL(baseUrl), diff --git a/app/[locale]/products/[id]/customization-form.tsx b/app/[locale]/products/[id]/customization-form.tsx index 9cf38260..e9ed8304 100644 --- a/app/[locale]/products/[id]/customization-form.tsx +++ b/app/[locale]/products/[id]/customization-form.tsx @@ -60,7 +60,6 @@ export function CustomizationForm({ const config = ProductCustomizationConfig.fromJson(customizationConfig); const textErrorId = errors.text ? 'customization-text-error' : undefined; - const _colorErrorId = errors.color ? 'customization-color-error' : undefined; const sizeErrorId = errors.size ? 'customization-size-error' : undefined; return ( diff --git a/app/[locale]/products/[id]/photo-upload-field.tsx b/app/[locale]/products/[id]/photo-upload-field.tsx index 87abbcbe..fef0d6a0 100644 --- a/app/[locale]/products/[id]/photo-upload-field.tsx +++ b/app/[locale]/products/[id]/photo-upload-field.tsx @@ -55,7 +55,7 @@ export function PhotoUploadField({ setError(null); startTransition(() => { - void onUpload(file).then((result) => { + onUpload(file).then((result) => { onUploaded?.(result); }); }); diff --git a/app/[locale]/profile/page.tsx b/app/[locale]/profile/page.tsx index 2ac801d0..a46ea82f 100644 --- a/app/[locale]/profile/page.tsx +++ b/app/[locale]/profile/page.tsx @@ -96,6 +96,10 @@ export default function ProfilePage() { }; }, [status, locale, router]); + if (status === 'loading' || loading) { + return
{dict.common.loading}
; + } + const handleSave = async (e: React.FormEvent) => { e.preventDefault(); setSaving(true); @@ -136,7 +140,7 @@ export default function ProfilePage() { throw new Error(data.error || 'Failed to delete account'); } // Redirect to home after soft-delete - window.location.href = '/'; + window.location.assign('/'); } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Failed to delete account'); } finally { @@ -145,10 +149,6 @@ export default function ProfilePage() { } }; - if (status === 'loading' || loading) { - return
{dict.common.loading}
; - } - return (
{dict.common.backToHome} diff --git a/app/api/uploads/local/[...storageKey]/route.ts b/app/api/uploads/local/[...storageKey]/route.ts index 118b5739..654441ca 100644 --- a/app/api/uploads/local/[...storageKey]/route.ts +++ b/app/api/uploads/local/[...storageKey]/route.ts @@ -1,10 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; async function getStorageRoot(): Promise { - const { join } = await import('node:path'); + const path = await import('node:path'); return ( process.env.LOCAL_UPLOAD_STORAGE_DIR ?? - join(process.cwd(), 'tmp', 'uploads') + path.join(process.cwd(), 'tmp', 'uploads') ); } @@ -12,7 +12,7 @@ async function resolveStoragePath( storageKey: string[], root: string, ): Promise { - const { resolve, relative, isAbsolute } = await import('node:path'); + const path = await import('node:path'); const decodedKey = decodeURIComponent(storageKey.join('/')); if ( @@ -24,14 +24,18 @@ async function resolveStoragePath( } const segments = decodedKey.split('/').filter(Boolean); - const rawPath = resolve(root, ...segments); - const rawRelative = relative(root, rawPath); - if (rawRelative.startsWith('..') || isAbsolute(rawRelative)) { + const rawPath = path.resolve(root, ...segments); + const rawRelative = path.relative(root, rawPath); + if ( + rawRelative === '..' || + rawRelative.startsWith(`..${path.sep}`) || + path.isAbsolute(rawRelative) + ) { return null; } const sanitized = segments.map((s) => s.replace(/[:*?"<>|]/g, '_')); - const filePath = resolve(root, ...sanitized); + const filePath = path.resolve(root, ...sanitized); return filePath; } @@ -56,7 +60,7 @@ export async function PUT( } const { mkdir, writeFile } = await import('node:fs/promises'); - const { dirname } = await import('node:path'); + const path = await import('node:path'); const { storageKey } = await context.params; const root = await getStorageRoot(); @@ -65,7 +69,7 @@ export async function PUT( return NextResponse.json({ error: 'Invalid storage key' }, { status: 400 }); } - await mkdir(dirname(filePath), { recursive: true }); + await mkdir(path.dirname(filePath), { recursive: true }); const body = Buffer.from(await request.arrayBuffer()); await writeFile(filePath, body); diff --git a/composition-root/container.ts b/composition-root/container.ts index 14c92b3a..3784f13b 100644 --- a/composition-root/container.ts +++ b/composition-root/container.ts @@ -155,11 +155,10 @@ let _searchHistoryEventsSubscribed = false; export function initContainer(): void { // --- EmailSender: env-dependent (Brevo in production, console otherwise) --- if (!_emailSender) { - if (process.env.NODE_ENV === 'production') { - _emailSender = new BrevoEmailSender(); - } else { - _emailSender = new ConsoleEmailSender(); - } + _emailSender = + process.env.NODE_ENV === 'production' + ? new BrevoEmailSender() + : new ConsoleEmailSender(); } // --- OutboxRepository: single Prisma adapter works in every env --- diff --git a/eslint.config.mjs b/eslint.config.mjs index 384fdc7f..fd5f2cbe 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -150,6 +150,7 @@ export default [ 'unicorn/no-null': 'off', // muchos proyectos usan null intencionalmente (ej. React) 'unicorn/prefer-module': 'off', // si tenéis algún archivo CJS (configs, scripts) 'unicorn/name-replacements': 'off', + 'unicorn/import-style': 'off', 'unicorn/no-top-level-assignment-in-function': 'off', 'unicorn/consistent-class-member-order': 'off', @@ -176,20 +177,6 @@ export default [ 'sonarjs/no-nested-template-literals': 'off', 'unicorn/no-nested-ternary': 'off', 'unicorn/no-array-sort': 'off', - 'unicorn/prefer-number-is-safe-integer': 'off', - 'unicorn/prefer-ternary': 'off', - 'sonarjs/no-useless-react-setstate': 'off', - 'sonarjs/no-hardcoded-passwords': 'off', - 'unicorn/prefer-add-event-listener': 'off', - 'sonarjs/super-linear-regex': 'off', - 'unicorn/numeric-separators-style': 'off', - 'sonarjs/void-use': 'off', - 'unicorn/import-style': 'off', - 'sonarjs/no-unused-vars': 'off', - 'unicorn/no-declarations-before-early-exit': 'off', - 'unicorn/prefer-string-raw': 'off', - 'unicorn/no-for-each': 'off', - 'unicorn/prefer-location-assign': 'off', // SonarJS: ajustar el umbral de complejidad cognitiva si el default es muy estricto // 'sonarjs/cognitive-complexity': ['warn', 15], diff --git a/modules/auth/infrastructure/memory-used-reset-token-store.ts b/modules/auth/infrastructure/memory-used-reset-token-store.ts index 0ec1717a..01443d93 100644 --- a/modules/auth/infrastructure/memory-used-reset-token-store.ts +++ b/modules/auth/infrastructure/memory-used-reset-token-store.ts @@ -34,6 +34,6 @@ export class MemoryUsedResetTokenStore implements UsedResetTokenStorePort { /** Mark a token as used with optional expiry for auto-cleanup. */ markTokenUsed(jti: string, exp?: number): void { this.purgeExpired(); - this.usedTokens.set(jti, exp ?? Date.now() + 3600_000); // default 1h TTL + this.usedTokens.set(jti, exp ?? Date.now() + 3_600_000); // default 1h TTL } } diff --git a/modules/cart/application/add-item-to-cart.ts b/modules/cart/application/add-item-to-cart.ts index 33df2e3c..6e0aee97 100644 --- a/modules/cart/application/add-item-to-cart.ts +++ b/modules/cart/application/add-item-to-cart.ts @@ -170,7 +170,7 @@ export class AddItemToCart { private async validateCustomizations( customizationIdList: string[], productId: string, - sellerId: string, + _sellerId: string, ): Promise { const snapshots = await this.customizationLookup.findByIds(customizationIdList); @@ -197,7 +197,6 @@ export class AddItemToCart { // their sellerId from the Product. If productId matches, the // sellerId is guaranteed to match (enforced at customization // creation time by the customizations module). - void sellerId; } } diff --git a/modules/cart/application/checkout-cart.ts b/modules/cart/application/checkout-cart.ts index 565d7bff..4f241ca8 100644 --- a/modules/cart/application/checkout-cart.ts +++ b/modules/cart/application/checkout-cart.ts @@ -95,11 +95,7 @@ export class CheckoutCart { ) {} async preview(userId: string): Promise { - const { cart, totals } = await this.buildTotals(userId, false); - // Touch the unused cart ref so TS doesn't complain about a return-only - // branch — the spec ties the preview to the cart's contents and we - // want the call to also serve as a validation gate. - void cart; + const { totals } = await this.buildTotals(userId, false); return totals; } @@ -269,14 +265,14 @@ export class CheckoutCart { // Persist the updated snapshots (if any) so the cart reflects the // new prices. Status stays ACTIVE — checkout hasn't run yet. - let liveCart = cart; - if (acceptPriceChanges && priceChanges.length > 0) { - liveCart = await this.cartRepository.save({ - ...cart, - items: updatedItems, - updatedAt: new Date(), - }); - } + const liveCart = + acceptPriceChanges && priceChanges.length > 0 + ? await this.cartRepository.save({ + ...cart, + items: updatedItems, + updatedAt: new Date(), + }) + : cart; // Compute totals from the live items (so acceptPriceChanges reflects // the updated snapshot prices in subtotal/discount/total). diff --git a/modules/cart/domain/value-objects/quantity.ts b/modules/cart/domain/value-objects/quantity.ts index 4c739b28..f31e31e1 100644 --- a/modules/cart/domain/value-objects/quantity.ts +++ b/modules/cart/domain/value-objects/quantity.ts @@ -20,7 +20,7 @@ export class Quantity { } static create(amount: number): Quantity { - if (!Number.isFinite(amount) || !Number.isInteger(amount)) { + if (!Number.isSafeInteger(amount)) { throw new InvalidQuantityError( `Quantity must be a finite integer, got: ${amount}`, ); diff --git a/modules/cart/presentation/components/add-to-cart-button.tsx b/modules/cart/presentation/components/add-to-cart-button.tsx index e04b3319..2acdce3e 100644 --- a/modules/cart/presentation/components/add-to-cart-button.tsx +++ b/modules/cart/presentation/components/add-to-cart-button.tsx @@ -492,6 +492,7 @@ export function AddToCartButton({ if (!isInCart || currentQuantity >= MAX_QUANTITY) return; const newQty = currentQuantity + 1; if (isAuthenticated && cartItemInfo) { + const prevCartItemInfo = cartItemInfo; setCartItemInfo({ ...cartItemInfo, quantity: newQty }); try { const res = await fetch(`/api/cart/items/${cartItemInfo.cartItemId}`, { @@ -500,12 +501,12 @@ export function AddToCartButton({ body: JSON.stringify({ quantity: newQty }), }); if (!res.ok) { - setCartItemInfo(cartItemInfo); + setCartItemInfo(prevCartItemInfo); return; } dispatchCartUpdated(); } catch { - setCartItemInfo(cartItemInfo); + setCartItemInfo(prevCartItemInfo); } } else if (guestMatch?.id) { updateItemQuantity(guestMatch.id, newQty); @@ -525,6 +526,7 @@ export function AddToCartButton({ const newQty = currentQuantity - 1; if (isAuthenticated && cartItemInfo) { + const prevCartItemInfo = cartItemInfo; setCartItemInfo({ ...cartItemInfo, quantity: newQty }); try { const res = await fetch(`/api/cart/items/${cartItemInfo.cartItemId}`, { @@ -533,12 +535,12 @@ export function AddToCartButton({ body: JSON.stringify({ quantity: newQty }), }); if (!res.ok) { - setCartItemInfo(cartItemInfo); + setCartItemInfo(prevCartItemInfo); return; } dispatchCartUpdated(); } catch { - setCartItemInfo(cartItemInfo); + setCartItemInfo(prevCartItemInfo); } } else if (guestMatch?.id) { updateItemQuantity(guestMatch.id, newQty); diff --git a/modules/cart/presentation/components/cart-icon.tsx b/modules/cart/presentation/components/cart-icon.tsx index 8ce8812c..38304e86 100644 --- a/modules/cart/presentation/components/cart-icon.tsx +++ b/modules/cart/presentation/components/cart-icon.tsx @@ -53,7 +53,7 @@ export function CartIcon({ alt }: CartIconProps) { useEffect(() => { if (!isAuthenticated) return; window.addEventListener(CART_UPDATED_EVENT, handleCartUpdated); - void Promise.try(fetchCount); + Promise.try(fetchCount); return () => { abortRef.current?.abort(); diff --git a/modules/customizations/application/create-customer-customization.ts b/modules/customizations/application/create-customer-customization.ts index 71400ad7..28eee752 100644 --- a/modules/customizations/application/create-customer-customization.ts +++ b/modules/customizations/application/create-customer-customization.ts @@ -57,16 +57,6 @@ export class CreateCustomerCustomization { config: ProductCustomizationConfig, ): void { const hasText = dto.text !== undefined && dto.text !== null; - const hasImage = dto.imageUrl !== undefined && dto.imageUrl !== null; - const hasDesignPosition = - dto.designPosition !== undefined && dto.designPosition !== null; - // A canvas-only customization is also a "has image" signal — the - // mockup canvas embeds imageUrl inside designPosition, so a - // canvas-only draft must satisfy the photo requirement. - const hasImageForCapability = hasImage || hasDesignPosition; - const hasStyle = - (dto.color !== undefined && dto.color !== null) || - (dto.size !== undefined && dto.size !== null); if (hasText && !config.allowsText()) { throw new ValidationError( @@ -75,6 +65,14 @@ export class CreateCustomerCustomization { ); } + const hasImage = dto.imageUrl !== undefined && dto.imageUrl !== null; + const hasDesignPosition = + dto.designPosition !== undefined && dto.designPosition !== null; + // A canvas-only customization is also a "has image" signal — the + // mockup canvas embeds imageUrl inside designPosition, so a + // canvas-only draft must satisfy the photo requirement. + const hasImageForCapability = hasImage || hasDesignPosition; + if (hasImageForCapability && !config.allowsPhoto()) { throw new ValidationError( 'This product does not support photo customization', @@ -82,6 +80,10 @@ export class CreateCustomerCustomization { ); } + const hasStyle = + (dto.color !== undefined && dto.color !== null) || + (dto.size !== undefined && dto.size !== null); + if (hasStyle && !config.allowsStyleOptions()) { throw new ValidationError( 'This product does not support color or size options', diff --git a/modules/customizations/domain/value-objects/customization-options.ts b/modules/customizations/domain/value-objects/customization-options.ts index 0603f078..4c89e4eb 100644 --- a/modules/customizations/domain/value-objects/customization-options.ts +++ b/modules/customizations/domain/value-objects/customization-options.ts @@ -55,15 +55,15 @@ export class CustomizationOptions { }): CustomizationOptions { // Normalize null to undefined — DB fields return string | null. const text = data.text ?? undefined; - const color = data.color ?? undefined; - const size = data.size ?? undefined; - const imageUrl = data.imageUrl ?? undefined; - const designPosition = data.designPosition ?? undefined; if (text !== undefined && text.length > 500) { throw new Error('Customization text must be at most 500 characters'); } + const color = data.color ?? undefined; + const size = data.size ?? undefined; + const imageUrl = data.imageUrl ?? undefined; + if (color !== undefined) { if (color.trim().length === 0) { throw new Error('Customization color cannot be empty if provided'); @@ -89,6 +89,8 @@ export class CustomizationOptions { } } + const designPosition = data.designPosition ?? undefined; + if ( designPosition !== undefined && designPosition !== null && diff --git a/modules/events/domain/event-registry.ts b/modules/events/domain/event-registry.ts index a5c07f5d..8fac1ddd 100644 --- a/modules/events/domain/event-registry.ts +++ b/modules/events/domain/event-registry.ts @@ -27,8 +27,10 @@ export const GlobalEvents = { /** User account deleted */ USER_DELETED: 'user.deleted', /** Password changed by authenticated user */ + // eslint-disable-next-line sonarjs/no-hardcoded-passwords PASSWORD_CHANGED: 'password.changed', /** Password reset via forgot-password flow */ + // eslint-disable-next-line sonarjs/no-hardcoded-passwords PASSWORD_RESET: 'password.reset', /** New seller created (initial status: active) */ SELLER_CREATED: 'seller.created', diff --git a/modules/orders/application/list-seller-orders-use-case.ts b/modules/orders/application/list-seller-orders-use-case.ts index 20dd8c2d..90a514c1 100644 --- a/modules/orders/application/list-seller-orders-use-case.ts +++ b/modules/orders/application/list-seller-orders-use-case.ts @@ -26,7 +26,8 @@ export class ListSellerOrdersUseCase { throw new NotFoundError('Seller not found'); } - const { userId: _userId, ...filter } = dto; + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- destructuring to exclude userId + const { userId, ...filter } = dto; return this.orderRepository.findPaginated( { ...filter, diff --git a/modules/presentation/components/design-preview.tsx b/modules/presentation/components/design-preview.tsx index 3bc53c6e..33876fe7 100644 --- a/modules/presentation/components/design-preview.tsx +++ b/modules/presentation/components/design-preview.tsx @@ -87,8 +87,8 @@ function loadImage(src: string): Promise { return new Promise((resolve) => { const img = new Image(); img.crossOrigin = 'anonymous'; - img.onload = () => resolve(img); - img.onerror = () => resolve(null); + img.addEventListener('load', () => resolve(img)); + img.addEventListener('error', () => resolve(null)); img.src = src; }); } diff --git a/modules/products/infrastructure/prisma-product-repository.ts b/modules/products/infrastructure/prisma-product-repository.ts index e84b857c..0f8ed8b2 100644 --- a/modules/products/infrastructure/prisma-product-repository.ts +++ b/modules/products/infrastructure/prisma-product-repository.ts @@ -204,7 +204,7 @@ export class PrismaProductRepository implements ProductRepository { let normQ = normalizeText(q); // Escape SQL ILIKE wildcards so user input like "100%" or "a_b" is // treated literally instead of expanding to unintended patterns. - normQ = normQ.replace(/%/g, '\\%').replace(/_/g, '\\_'); + normQ = normQ.replace(/%/g, String.raw`\%`).replace(/_/g, String.raw`\_`); // Parameterised placeholders ($1, $2) prevent SQL injection. const rows = await prisma.$queryRawUnsafe>( `SELECT DISTINCT p.id diff --git a/modules/sellers/application/use-cases/list-seller-products-use-case.ts b/modules/sellers/application/use-cases/list-seller-products-use-case.ts index 3c023647..dba672a2 100644 --- a/modules/sellers/application/use-cases/list-seller-products-use-case.ts +++ b/modules/sellers/application/use-cases/list-seller-products-use-case.ts @@ -40,7 +40,8 @@ export class ListSellerProductsUseCase { throw new NotFoundError('Seller not found'); } - const { userId: _userId, ...filter } = dto; + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- destructuring to exclude userId + const { userId, ...filter } = dto; return this.productQuery.execute({ ...filter, diff --git a/modules/uploads/infrastructure/prisma-upload-repository.ts b/modules/uploads/infrastructure/prisma-upload-repository.ts index 3d3070e0..2a915c80 100644 --- a/modules/uploads/infrastructure/prisma-upload-repository.ts +++ b/modules/uploads/infrastructure/prisma-upload-repository.ts @@ -54,7 +54,7 @@ export class PrismaUploadRepository implements UploadRepository { `findPendingOlderThan: hours must be a positive finite number, got ${hours}`, ); } - const cutoff = new Date(Date.now() - hours * 3600_000); + const cutoff = new Date(Date.now() - hours * 3_600_000); const rows = await prisma.upload.findMany({ where: { status: UploadStatus.PENDING, diff --git a/modules/users/application/use-cases/forgot-password-use-case.ts b/modules/users/application/use-cases/forgot-password-use-case.ts index 3a7d2e90..3f1cf914 100644 --- a/modules/users/application/use-cases/forgot-password-use-case.ts +++ b/modules/users/application/use-cases/forgot-password-use-case.ts @@ -7,7 +7,7 @@ export interface ForgotPasswordDTO { } /** Token validity: 1 hour in milliseconds. */ -const TOKEN_TTL_MS = 3600_000; +const TOKEN_TTL_MS = 3_600_000; export class ForgotPasswordUseCase { constructor( diff --git a/modules/users/application/use-cases/register-user-use-case.ts b/modules/users/application/use-cases/register-user-use-case.ts index 01427236..b52e6c19 100644 --- a/modules/users/application/use-cases/register-user-use-case.ts +++ b/modules/users/application/use-cases/register-user-use-case.ts @@ -60,15 +60,14 @@ export class RegisterUserUseCase { const userId = UserId.create(crypto.randomUUID()); const roleId = RoleId.create('CUSTOMER'); - let address: Address | null = null; - if (dto.address) { - address = Address.create( - dto.address.street, - dto.address.city, - dto.address.postalCode, - dto.address.country, - ); - } + const address = dto.address + ? Address.create( + dto.address.street, + dto.address.city, + dto.address.postalCode, + dto.address.country, + ) + : null; // 5. Save user const now = new Date(); diff --git a/modules/users/infrastructure/prisma-user-repository.ts b/modules/users/infrastructure/prisma-user-repository.ts index 9c496c68..d5317dfb 100644 --- a/modules/users/infrastructure/prisma-user-repository.ts +++ b/modules/users/infrastructure/prisma-user-repository.ts @@ -26,20 +26,18 @@ export function toDomain(user: { }): UserEntity { if (!user.email) throw new Error('User email is required'); - let address: Address | null = null; - if ( + const address = user.addressStreet && user.addressCity && user.addressPostalCode && user.addressCountry - ) { - address = Address.create( - user.addressStreet, - user.addressCity, - user.addressPostalCode, - user.addressCountry, - ); - } + ? Address.create( + user.addressStreet, + user.addressCity, + user.addressPostalCode, + user.addressCountry, + ) + : null; return { userId: UserId.create(user.id), diff --git a/proxy.ts b/proxy.ts index 6b0f56a7..a555f741 100644 --- a/proxy.ts +++ b/proxy.ts @@ -125,9 +125,7 @@ function unauthorizedResponse(request: NextRequest, pathname: string) { export const config = { matcher: [ - // Pages and non-api paths (including /img/ static assets) - '/((?!api|_next/static|_next/image|favicon\\.ico|icon\\.svg|.*\\.png$).*)', - // Protected API routes (explicitly added since the regex above excludes /api) + '/((?!api|_next/static|_next/image|favicon[.]ico|icon[.]svg|.*[.]png$).*)', '/api/admin/:path*', '/api/orders/:path*', '/api/users/:path*', diff --git a/scripts/optimize-sprites.mjs b/scripts/optimize-sprites.mjs index 17ba409c..f401562a 100644 --- a/scripts/optimize-sprites.mjs +++ b/scripts/optimize-sprites.mjs @@ -9,14 +9,14 @@ * ../shared/presentation/sprites.css (utility classes for sizing) */ import { readFileSync, writeFileSync } from 'node:fs'; -import { join, dirname } from 'node:path'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { optimize } from 'svgo'; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const ICONS_DIR = join(__dirname, '../devresources/icons'); -const OUTPUT_SVG = join(__dirname, '../public/img/icons/sprites.svg'); -const OUTPUT_CSS = join(__dirname, '../shared/presentation/sprites.css'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ICONS_DIR = path.join(__dirname, '../devresources/icons'); +const OUTPUT_SVG = path.join(__dirname, '../public/img/icons/sprites.svg'); +const OUTPUT_CSS = path.join(__dirname, '../shared/presentation/sprites.css'); // Icon name mapping: filename -> class/id name // Files are read directly from the /icons directory at repo root @@ -49,10 +49,18 @@ function resolveClassFills(svgContent) { const cssText = match[1]; const classMap = {}; - const ruleRegex = /\.(\S+)\s*\{[^}]*?fill:\s*([^;}]+)[^}]*?\}/g; - let m; - while ((m = ruleRegex.exec(cssText)) !== null) { - classMap[m[1]] = m[2].trim(); + for (const rule of cssText.split('}')) { + const dotIdx = rule.indexOf('.'); + const braceIdx = rule.indexOf('{'); + if (dotIdx === -1 || braceIdx === -1 || dotIdx > braceIdx) continue; + const className = rule + .slice(dotIdx + 1, braceIdx) + .trim() + .split(/\s+/)[0]; + const fillMatch = rule.slice(braceIdx).match(/fill:\s*([^;}\s]+)/); + if (className && fillMatch) { + classMap[className] = fillMatch[1].trim(); + } } if (Object.keys(classMap).length === 0) return svgContent; @@ -69,7 +77,7 @@ function resolveClassFills(svgContent) { // Optimize each icon and extract viewBox content const icons = files.map((file) => { - const raw = readFileSync(join(ICONS_DIR, file), 'utf8'); + const raw = readFileSync(path.join(ICONS_DIR, file), 'utf8'); // Resolve class-based fills to inline fills BEFORE optimization, // so SVGO doesn't strip the