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
6 changes: 6 additions & 0 deletions app/[locale]/admin/sellers/[sellerId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ export default async function AdminSellerDetailPage({
errorLabel={dict.admin.sellerDetail.error}
initialName={seller.name}
initialDescription={seller.description ?? ''}
unsavedChangesLabels={{
title: dict.common?.unsavedChangesTitle,
message: dict.common?.unsavedChangesMessage,
leave: dict.common?.unsavedChangesLeave,
stay: dict.common?.unsavedChangesStay,
}}
/>
</div>
);
Expand Down
192 changes: 119 additions & 73 deletions app/[locale]/products/[id]/customization-experience.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ import styles from './customization-experience.module.css';
import pageStyles from './page.module.css';
import { RoleAwarePurchaseFooter } from './role-aware-purchase-footer';
import type { ProductViewerContext } from '@/shared/authorization/product-viewer-context';
import { useUnsavedChangesGuard } from '@/shared/hooks/use-unsaved-changes-guard';
import {
resolvePhotoLabel,
type ProductTranslationEntity,
} from '@/modules/products/domain/entities/product-translation';

export interface ProductImageItem {
url: string;
alt: string;
id?: string;
label?: string;
purpose: ProductImagePurpose;
}

Expand Down Expand Up @@ -79,10 +86,16 @@ export interface CustomizationExperienceLabels {
mediaPrevious: string;
mediaNext: string;
goToEdit: string;
unsavedChangesTitle?: string;
unsavedChangesMessage?: string;
unsavedChangesLeave?: string;
unsavedChangesStay?: string;
}

interface CustomizationExperienceProps {
productId: string;
locale?: string;
translations?: ProductTranslationEntity[];
productName: string;
productDescription: string;
designChangeDescription?: string | null;
Expand All @@ -109,6 +122,8 @@ async function preloadAndDecodeImage(url: string): Promise<void> {

function CustomizationExperienceInner({
productId,
locale,
translations,
productName,
productDescription,
designChangeDescription,
Expand All @@ -122,10 +137,31 @@ function CustomizationExperienceInner({
productImages,
publicMedia,
labels,
initialDraft,
editCartItemId,
viewerContext,
}: CustomizationExperienceProps) {
const { draft, setImage, setDesignPosition } = useCustomizationDraft();
const initialDraftSnapshot = {
text: initialDraft?.text ?? null,
color: initialDraft?.color ?? null,
size: initialDraft?.size ?? null,
imageUploadId: initialDraft?.imageUploadId ?? null,
imageUrl: initialDraft?.imageUrl ?? null,
designPosition: initialDraft?.designPosition ?? null,
error: null,
};
const unsavedGuard = useUnsavedChangesGuard(
JSON.stringify(draft) !== JSON.stringify(initialDraftSnapshot),
{
title: labels.unsavedChangesTitle ?? 'Unsaved changes',
message:
labels.unsavedChangesMessage ??
'You have unsaved changes. Leave this page?',
leave: labels.unsavedChangesLeave ?? 'Leave',
stay: labels.unsavedChangesStay ?? 'Stay',
},
);
Comment on lines +145 to +164

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

El campo transitorio error en la comparación de cambios puede generar falsos positivos.

initialDraftSnapshot incluye error: null y la comparación JSON.stringify(draft) !== JSON.stringify(initialDraftSnapshot) abarca ese campo. Si validateDraft() se ejecuta (al enviar el formulario) y la validación falla, draft.error pasa a un valor no nulo aunque el usuario no haya modificado ningún campo, activando el guard de navegación sin cambios reales.

🔧 Propuesta de fix: normalizar `error` antes de comparar
   const unsavedGuard = useUnsavedChangesGuard(
-    JSON.stringify(draft) !== JSON.stringify(initialDraftSnapshot),
+    JSON.stringify({ ...draft, error: null }) !==
+      JSON.stringify(initialDraftSnapshot),
     {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const initialDraftSnapshot = {
text: initialDraft?.text ?? null,
color: initialDraft?.color ?? null,
size: initialDraft?.size ?? null,
imageUploadId: initialDraft?.imageUploadId ?? null,
imageUrl: initialDraft?.imageUrl ?? null,
designPosition: initialDraft?.designPosition ?? null,
error: null,
};
const unsavedGuard = useUnsavedChangesGuard(
JSON.stringify(draft) !== JSON.stringify(initialDraftSnapshot),
{
title: labels.unsavedChangesTitle ?? 'Unsaved changes',
message:
labels.unsavedChangesMessage ??
'You have unsaved changes. Leave this page?',
leave: labels.unsavedChangesLeave ?? 'Leave',
stay: labels.unsavedChangesStay ?? 'Stay',
},
);
const initialDraftSnapshot = {
text: initialDraft?.text ?? null,
color: initialDraft?.color ?? null,
size: initialDraft?.size ?? null,
imageUploadId: initialDraft?.imageUploadId ?? null,
imageUrl: initialDraft?.imageUrl ?? null,
designPosition: initialDraft?.designPosition ?? null,
error: null,
};
const unsavedGuard = useUnsavedChangesGuard(
JSON.stringify({ ...draft, error: null }) !==
JSON.stringify(initialDraftSnapshot),
{
title: labels.unsavedChangesTitle ?? 'Unsaved changes',
message:
labels.unsavedChangesMessage ??
'You have unsaved changes. Leave this page?',
leave: labels.unsavedChangesLeave ?? 'Leave',
stay: labels.unsavedChangesStay ?? 'Stay',
},
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/customization-experience.tsx around lines 145 -
164, Exclude the transient error field from the unsaved-changes comparison
around initialDraftSnapshot and useUnsavedChangesGuard. Normalize or remove
draft.error before comparing it with the initial snapshot so validation failures
do not trigger the guard when no editable fields changed.

const customizationModel =
ProductCustomizationConfig.fromJson(customizationConfig);
const resolvedCustomizationConfig =
Expand All @@ -134,6 +170,13 @@ function CustomizationExperienceInner({
const customizableBaseImages = productImages.filter(
(image) => image.purpose === ProductImagePurpose.CUSTOMIZABLE_BASE,
);
const labeledImages = customizableBaseImages.map((image) => ({
...image,
label:
(image.id
? resolvePhotoLabel(translations ?? [], image.id, locale ?? 'es')
: '') || image.alt,
}));
const activeProductImageUrl =
customizableBaseImages.find((img) => img.alt === draft.color)?.url ??
previewBaseImageUrl;
Expand Down Expand Up @@ -263,84 +306,87 @@ function CustomizationExperienceInner({
};

return (
<Card as="section" padding="lg" className={styles.card}>
<div className={styles.layout} data-testid="purchase-layout">
<aside
className={styles.presentationColumn}
data-testid="purchase-layout-right"
>
<header className={styles.header}>
<span className={pageStyles.seller}>{sellerName}</span>
<h1 className={pageStyles.title}>{productName}</h1>
<p className={pageStyles.description}>{productDescription}</p>
</header>
<>
{unsavedGuard}
<Card as="section" padding="lg" className={styles.card}>
<div className={styles.layout} data-testid="purchase-layout">
<aside
className={styles.presentationColumn}
data-testid="purchase-layout-right"
>
<header className={styles.header}>
<span className={pageStyles.seller}>{sellerName}</span>
<h1 className={pageStyles.title}>{productName}</h1>
<p className={pageStyles.description}>{productDescription}</p>
</header>

<ProductShowcaseGallery
items={publicMedia}
labels={{
previous: labels.mediaPrevious,
next: labels.mediaNext,
}}
/>
</aside>

<section
className={styles.purchaseColumn}
data-testid="purchase-layout-left"
>
<div className={styles.formCol}>
<CustomizationForm
customizationConfig={resolvedCustomizationConfig}
sizes={sizes}
productImages={customizableBaseImages}
labels={formLabels}
helpText={designChangeDescription}
<ProductShowcaseGallery
items={publicMedia}
labels={{
previous: labels.mediaPrevious,
next: labels.mediaNext,
}}
/>
</div>
</aside>

<div className={styles.canvasCol} data-testid="mockup-canvas">
{isAllowsPhoto && previewBaseImageUrl && (
<MockupCanvasControl
productImageUrl={displayedProductImageUrl}
initialDesignUrl={draft.imageUrl}
initialPosition={draft.designPosition}
labels={mockupLabels}
onUpload={uploadDesign}
onPositionChange={setDesignPosition}
<section
className={styles.purchaseColumn}
data-testid="purchase-layout-left"
>
<div className={styles.formCol}>
<CustomizationForm
customizationConfig={resolvedCustomizationConfig}
sizes={sizes}
productImages={labeledImages}
labels={formLabels}
helpText={designChangeDescription}
/>
)}
</div>
</section>
</div>

<div className={styles.canvasCol} data-testid="mockup-canvas">
{isAllowsPhoto && previewBaseImageUrl && (
<MockupCanvasControl
productImageUrl={displayedProductImageUrl}
initialDesignUrl={draft.imageUrl}
initialPosition={draft.designPosition}
labels={mockupLabels}
onUpload={uploadDesign}
onPositionChange={setDesignPosition}
/>
)}
</div>
</section>

<footer className={styles.footer}>
<p className={styles.price}>{formattedPrice}</p>
<RoleAwarePurchaseFooter
viewerContext={viewerContext ?? anonymousViewerContext}
editLabel={labels.goToEdit}
cart={{
productId,
productName,
sellerId,
sellerName,
price,
imageUrl: displayedProductImageUrl,
customizationAvailable: !customizationModel.isDefault(),
customizeHref: '#customization-form',
labels: cartLabels,
editCartItemId,
customization: {
text: draft.text,
color: draft.color,
size: draft.size,
imageUploadId: draft.imageUploadId,
imageUrl: draft.imageUrl,
designPosition: draft.designPosition,
},
}}
/>
</footer>
</div>
</Card>
<footer className={styles.footer}>
<p className={styles.price}>{formattedPrice}</p>
<RoleAwarePurchaseFooter
viewerContext={viewerContext ?? anonymousViewerContext}
editLabel={labels.goToEdit}
cart={{
productId,
productName,
sellerId,
sellerName,
price,
imageUrl: displayedProductImageUrl,
customizationAvailable: !customizationModel.isDefault(),
customizeHref: '#customization-form',
labels: cartLabels,
editCartItemId,
customization: {
text: draft.text,
color: draft.color,
size: draft.size,
imageUploadId: draft.imageUploadId,
imageUrl: draft.imageUrl,
designPosition: draft.designPosition,
},
}}
/>
</footer>
</div>
</Card>
</>
);
}

Expand Down
6 changes: 4 additions & 2 deletions app/[locale]/products/[id]/customization-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,17 @@ export function CustomizationForm({
e.preventDefault();
setColor(img.alt);
}}
title={img.alt}
title={img.label || undefined}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={img.url}
alt={img.alt}
className={formStyles.colorThumb}
/>
<span className={formStyles.colorAlt}>{img.alt}</span>
<span className={formStyles.colorAlt}>
{img.label || img.alt}
</span>
</div>
);
})}
Expand Down
7 changes: 7 additions & 0 deletions app/[locale]/products/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ export default async function ProductDetailPage({
mediaPrevious: dict.orders?.previous ?? 'Previous',
mediaNext: dict.orders?.next ?? 'Next',
goToEdit: dict.common.goToEdit,
unsavedChangesTitle: dict.common.unsavedChangesTitle,
unsavedChangesMessage: dict.common.unsavedChangesMessage,
unsavedChangesLeave: dict.common.unsavedChangesLeave,
unsavedChangesStay: dict.common.unsavedChangesStay,
} satisfies CustomizationExperienceLabels;
customizationLabels.customizationDesign = isDesigner
? dict.common.customizationDesignDesigner
Expand Down Expand Up @@ -180,6 +184,7 @@ export default async function ProductDetailPage({
})),
];
const customizableBaseImages = view.customizableBase.map((image) => ({
id: image.id,
url: image.url,
alt: image.alt ?? product.displayName,
purpose: image.purpose,
Expand All @@ -194,6 +199,8 @@ export default async function ProductDetailPage({
<div className={styles.detailLayout}>
<CustomizationExperience
productId={product.id}
locale={locale}
translations={product.translations}
productName={product.displayName}
productDescription={product.displayDescription}
designChangeDescription={
Expand Down
3 changes: 3 additions & 0 deletions app/[locale]/seller/products/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export default async function SellerProductEditPage({
sizes: [...(translation.sizes ?? [])],
designChangeDescription:
translation.designChangeDescription ?? null,
photoLabels: translation.photoLabels
? { ...translation.photoLabels }
: {},
};
}),
],
Expand Down
Loading
Loading