Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions app/[locale]/products/[id]/customization-experience.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,17 +331,18 @@ function CustomizationExperienceInner({
<header className={styles.header}>
<span className={pageStyles.seller}>{sellerName}</span>
<h1 className={pageStyles.title}>{productName}</h1>
{categoryLink && (
<Link
href={`/${locale}?category=${categoryLink.slug}`}
className={styles.categoryLink}
>
{labels.categoryLabel}: {categoryLink.name}
</Link>
)}
<p className={pageStyles.description}>{productDescription}</p>
</header>

{categoryLink && (
<Link
href={`/${locale}?category=${categoryLink.slug}`}
className={styles.categoryLink}
>
{labels.categoryLabel}: {categoryLink.name}
</Link>
)}

<ProductShowcaseGallery
items={publicMedia}
labels={{
Expand Down
9 changes: 5 additions & 4 deletions app/[locale]/products/[id]/mockup-canvas-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import {
MIN_DESIGN_SCALE_PERCENT,
type DesignPosition,
} from '@/modules/products/domain/value-objects/product-customization-config';
import { drawCover, drawDesign } from '@/shared/presentation/canvas-utils';
import { drawContain, drawDesign } from '@/shared/presentation/canvas-utils';
import styles from './mockup-canvas-control.module.css';

const CANVAS_WIDTH = 300;
const CANVAS_HEIGHT = 225;
// Keep enough backing pixels for the responsive canvas when the device DPR is 1.
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 450;

interface MockupCanvasControlLabels {
canvasLabel: string;
Expand Down Expand Up @@ -129,7 +130,7 @@ export function MockupCanvasControl({
productImage.complete &&
productImage.naturalWidth > 0
) {
drawCover(ctx, productImage, CANVAS_WIDTH, CANVAS_HEIGHT);
drawContain(ctx, productImage, CANVAS_WIDTH, CANVAS_HEIGHT);
}

const designImage = designImageRef.current;
Expand Down
1 change: 1 addition & 0 deletions components/products/infinite-product-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export function InfiniteProductList({
height={640}
unoptimized
className={styles.productCoverImage}
crossOrigin="anonymous"
/>
) : (
<div className={styles.productCoverPlaceholder}>
Expand Down
4 changes: 2 additions & 2 deletions modules/presentation/components/design-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useEffect, useRef } from 'react';
import {
loadImage,
drawCover,
drawContain,
drawDesign,
} from '@/shared/presentation/canvas-utils';

Expand Down Expand Up @@ -52,7 +52,7 @@ export function DesignPreview({
c.fillRect(0, 0, width, height);

if (productImg) {
drawCover(c, productImg, width, height);
drawContain(c, productImg, width, height);
}

if (designImg) {
Expand Down
23 changes: 17 additions & 6 deletions modules/products/application/update-product-use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@ function buildTranslations(
throw new ValidationError('Product locale is required');
}

const name = (translation.name ?? dto.name ?? '').trim();
const existingTranslation = merged.get(locale);
const name = (
translation.name ??
dto.name ??
existingTranslation?.name ??
''
).trim();

if (!name) {
throw new ValidationError('Product name is required');
Expand All @@ -116,14 +122,19 @@ function buildTranslations(
merged.set(locale, {
locale,
name,
description: translation.description?.trim() || null,
tags: translation.tags ?? [],
sizes: translation.sizes ?? [],
description:
translation.description === undefined
? (existingTranslation?.description ?? null)
: translation.description.trim() || null,
tags: translation.tags ?? existingTranslation?.tags ?? [],
sizes: translation.sizes ?? existingTranslation?.sizes ?? [],
designChangeDescription:
translation.designChangeDescription?.trim() || null,
translation.designChangeDescription === undefined
? (existingTranslation?.designChangeDescription ?? null)
: translation.designChangeDescription?.trim() || null,
photoLabels:
cleanPhotoLabels(translation.photoLabels, product) ??
merged.get(locale)?.photoLabels ??
existingTranslation?.photoLabels ??
{},
});
}
Expand Down
8 changes: 4 additions & 4 deletions shared/presentation/canvas-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function loadImage(src: string): Promise<HTMLImageElement | null> {
});
}

export function drawCover(
export function drawContain(
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
width: number,
Expand All @@ -20,11 +20,11 @@ export function drawCover(
let drawHeight: number;

if (ratio > targetRatio) {
drawHeight = height;
drawWidth = height * ratio;
} else {
drawWidth = width;
drawHeight = width / ratio;
} else {
drawHeight = height;
drawWidth = height * ratio;
}

const offsetX = (width - drawWidth) / 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,29 @@ describe('CustomizationExperience', () => {
expect(props.customization.imageUrl).toBe('/upload.png');
});

it('places the category link between the description and gallery', () => {
render(
<CustomizationExperience
{...commonProps}
customizationConfig={ProductCustomizationConfig.default().toJson()}
labels={labels}
/>,
);

const categoryLink = screen.getByRole('link', { name: 'Category: Mugs' });
const description = screen.getByText('A nice mug');
const gallery = screen.getByTestId('showcase-gallery');

expect(
description.compareDocumentPosition(categoryLink) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(
categoryLink.compareDocumentPosition(gallery) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
});

it('keeps the previous image until the selected image is decoded', async () => {
const decodeResolvers: Array<() => void> = [];
class MockImage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ describe('MockupCanvasControl backing store', () => {
const { getByLabelText } = renderCanvas();
const canvas = getByLabelText('Canvas') as HTMLCanvasElement;

expect(canvas.width).toBe(600);
expect(canvas.height).toBe(450);
expect(canvas.width).toBe(1200);
expect(canvas.height).toBe(900);
expect(canvas.style.width).toBe('');
expect(canvas.style.height).toBe('');
expect(context.setTransform).toHaveBeenCalledWith(2, 0, 0, 2, 0, 0);
Expand All @@ -80,17 +80,17 @@ describe('MockupCanvasControl backing store', () => {
const { getByLabelText } = renderCanvas();
const canvas = getByLabelText('Canvas') as HTMLCanvasElement;

expect(canvas.width).toBe(300);
expect(canvas.height).toBe(225);
expect(canvas.width).toBe(600);
expect(canvas.height).toBe(450);

Object.defineProperty(globalThis, 'devicePixelRatio', {
configurable: true,
value: 3,
});
globalThis.dispatchEvent(new Event('resize'));

expect(canvas.width).toBe(900);
expect(canvas.height).toBe(675);
expect(canvas.width).toBe(1800);
expect(canvas.height).toBe(1350);
expect(context.setTransform).toHaveBeenLastCalledWith(3, 0, 0, 3, 0, 0);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@ describe('InfiniteProductList', () => {
/>,
);

expect(screen.getByAltText('Mug cover')).toBeInTheDocument();
expect(screen.getByAltText('Mug cover')).toHaveAttribute(
'crossorigin',
'anonymous',
);
});

it('shows a placeholder when a cover is missing', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,32 @@ describe('UpdateProductUseCase', () => {
);
});

it('preserves an untouched locale and omitted fields from a partial update', async () => {
const repo = new MemoryProductRepository();
repo.seed([makeProduct()]);

const result = await new UpdateProductUseCase(repo).execute({
productId: 'p-1',
sellerId: 'seller-1',
translations: [{ locale: 'es', name: 'Taza editada' }],
});

expect(result.translations).toEqual(
expect.arrayContaining([
expect.objectContaining({
locale: 'es',
name: 'Taza editada',
description: 'Una taza',
}),
expect.objectContaining({
locale: 'cat',
name: 'Tassa',
description: 'Una tassa',
}),
]),
);
});

it('cleans stale photo label keys and preserves omitted locale labels', async () => {
const repo = new MemoryProductRepository();
repo.seed([
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/shared/presentation/canvas-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it, vi } from 'vitest';
import { drawContain } from '@/shared/presentation/canvas-utils';

function drawImageFor(naturalWidth: number, naturalHeight: number) {
const ctx = { drawImage: vi.fn() } as unknown as CanvasRenderingContext2D;
const image = { naturalWidth, naturalHeight } as HTMLImageElement;

drawContain(ctx, image, 600, 450);

return ctx.drawImage;
}

describe('drawContain', () => {
it('fits a wide image and centers it with vertical margins', () => {
expect(drawImageFor(1600, 900)).toHaveBeenCalledWith(
expect.anything(),
0,
56.25,
600,
337.5,
);
});

it('fits a tall image and centers it with horizontal margins', () => {
expect(drawImageFor(900, 1600)).toHaveBeenCalledWith(
expect.anything(),
173.4375,
0,
253.125,
450,
);
});
});
Loading