diff --git a/frontend/src/components/ElementSettings/useElementSettingsForm.ts b/frontend/src/components/ElementSettings/useElementSettingsForm.ts index 73233ba..74b3bde 100644 --- a/frontend/src/components/ElementSettings/useElementSettingsForm.ts +++ b/frontend/src/components/ElementSettings/useElementSettingsForm.ts @@ -334,20 +334,20 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { galleryTextFontFamily: String(settings.galleryTextFontFamily || ''), galleryCards: Array.isArray(settings.galleryCards) ? settings.galleryCards.map( - (card: Record, index: number) => ({ + (card: Record) => ({ id: String(card?.id || createLocalId()), - imageUrl: String(card?.imageUrl || ''), - title: String(card?.title || `Card ${index + 1}`), - description: String(card?.description || ''), + imageUrl: String(card?.imageUrl ?? ''), + title: String(card?.title ?? ''), + description: String(card?.description ?? ''), }), ) : [], carouselSlides: Array.isArray(settings.carouselSlides) ? settings.carouselSlides.map( - (slide: Record, index: number) => ({ + (slide: Record) => ({ id: String(slide?.id || createLocalId()), - imageUrl: String(slide?.imageUrl || ''), - caption: String(slide?.caption || `Slide ${index + 1}`), + imageUrl: String(slide?.imageUrl ?? ''), + caption: String(slide?.caption ?? ''), }), ) : [], @@ -687,7 +687,7 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { settings.galleryCards = state.galleryCards.map((card, index) => ({ id: String(card.id || createLocalId()), imageUrl: card.imageUrl.trim(), - title: card.title.trim() || `Card ${index + 1}`, + title: card.title.trim(), description: card.description, })); settings.galleryTitleFontFamily = state.galleryTitleFontFamily.trim(); @@ -696,10 +696,10 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { // Carousel type settings if (isCarouselType) { - settings.carouselSlides = state.carouselSlides.map((slide, index) => ({ + settings.carouselSlides = state.carouselSlides.map((slide) => ({ id: String(slide.id || createLocalId()), imageUrl: slide.imageUrl.trim(), - caption: slide.caption.trim() || `Slide ${index + 1}`, + caption: slide.caption.trim(), })); settings.carouselPrevIconUrl = state.carouselPrevIconUrl.trim(); settings.carouselNextIconUrl = state.carouselNextIconUrl.trim(); diff --git a/frontend/src/hooks/useConstructorElements.ts b/frontend/src/hooks/useConstructorElements.ts index 416ac44..655aac0 100644 --- a/frontend/src/hooks/useConstructorElements.ts +++ b/frontend/src/hooks/useConstructorElements.ts @@ -70,8 +70,12 @@ interface UseConstructorElementsResult { clearSelection: () => void; /** Add a new element of the given type */ addElement: (type: CanvasElementType) => void; - /** Update the selected element with a partial patch */ - updateSelectedElement: (patch: Partial) => void; + /** Update the selected element with a partial patch (or callback for fresh state) */ + updateSelectedElement: ( + patchOrFn: + | Partial + | ((current: CanvasElement) => Partial), + ) => void; /** Update an element by ID with a partial patch */ updateElement: (elementId: string, patch: Partial) => void; /** Remove the selected element */ @@ -227,13 +231,20 @@ export function useConstructorElements({ ); const updateSelectedElement = useCallback( - (patch: Partial) => { + ( + patchOrFn: + | Partial + | ((current: CanvasElement) => Partial), + ) => { if (!selectedElementId) return; setElements((prev) => { - const next = prev.map((el) => - el.id === selectedElementId ? { ...el, ...patch } : el, - ); + const next = prev.map((el) => { + if (el.id !== selectedElementId) return el; + const patch = + typeof patchOrFn === 'function' ? patchOrFn(el) : patchOrFn; + return { ...el, ...patch }; + }); onElementsChange?.(next); return next; }); @@ -328,10 +339,12 @@ export function useConstructorElements({ update: (cardId: string, patch: Partial) => { if (!selectedElement || !isGalleryElementType(selectedElement.type)) return; - const nextCards = (selectedElement.galleryCards || []).map((card) => - card.id === cardId ? { ...card, ...patch } : card, - ); - updateSelectedElement({ galleryCards: nextCards }); + // Use callback to get fresh element state (avoids stale closure on rapid updates) + updateSelectedElement((current) => ({ + galleryCards: (current.galleryCards || []).map((card) => + card.id === cardId ? { ...card, ...patch } : card, + ), + })); }, remove: (cardId: string) => { if (!selectedElement || !isGalleryElementType(selectedElement.type)) @@ -363,10 +376,12 @@ export function useConstructorElements({ update: (spanId: string, patch: Partial) => { if (!selectedElement || !isGalleryElementType(selectedElement.type)) return; - const nextSpans = (selectedElement.galleryInfoSpans || []).map( - (span) => (span.id === spanId ? { ...span, ...patch } : span), - ); - updateSelectedElement({ galleryInfoSpans: nextSpans }); + // Use callback to get fresh element state (avoids stale closure on rapid updates) + updateSelectedElement((current) => ({ + galleryInfoSpans: (current.galleryInfoSpans || []).map((span) => + span.id === spanId ? { ...span, ...patch } : span, + ), + })); }, remove: (spanId: string) => { if (!selectedElement || !isGalleryElementType(selectedElement.type)) @@ -399,10 +414,12 @@ export function useConstructorElements({ update: (slideId: string, patch: Partial) => { if (!selectedElement || !isCarouselElementType(selectedElement.type)) return; - const nextSlides = (selectedElement.carouselSlides || []).map( - (slide) => (slide.id === slideId ? { ...slide, ...patch } : slide), - ); - updateSelectedElement({ carouselSlides: nextSlides }); + // Use callback to get fresh element state (avoids stale closure on rapid updates) + updateSelectedElement((current) => ({ + carouselSlides: (current.carouselSlides || []).map((slide) => + slide.id === slideId ? { ...slide, ...patch } : slide, + ), + })); }, remove: (slideId: string) => { if (!selectedElement || !isCarouselElementType(selectedElement.type)) diff --git a/frontend/src/lib/elementDefaults.ts b/frontend/src/lib/elementDefaults.ts index 380b314..ff44d05 100644 --- a/frontend/src/lib/elementDefaults.ts +++ b/frontend/src/lib/elementDefaults.ts @@ -224,12 +224,11 @@ export const createDefaultElement = ( */ export const normalizeGalleryCard = ( card: Record, - index: number, ): GalleryCard => ({ id: String(card?.id || createLocalId()), - imageUrl: String(card?.imageUrl || ''), - title: String(card?.title || `Card ${index + 1}`), - description: String(card?.description || ''), + imageUrl: String(card?.imageUrl ?? ''), + title: String(card?.title ?? ''), + description: String(card?.description ?? ''), }); /** @@ -239,7 +238,7 @@ export const normalizeGalleryInfoSpan = ( span: Record, ): GalleryInfoSpan => ({ id: String(span?.id || createLocalId()), - text: String(span?.text || ''), + text: String(span?.text ?? ''), iconUrl: span?.iconUrl ? String(span.iconUrl) : undefined, }); @@ -248,11 +247,10 @@ export const normalizeGalleryInfoSpan = ( */ export const normalizeCarouselSlide = ( slide: Record, - index: number, ): CarouselSlide => ({ id: String(slide?.id || createLocalId()), - imageUrl: String(slide?.imageUrl || ''), - caption: String(slide?.caption || `Slide ${index + 1}`), + imageUrl: String(slide?.imageUrl ?? ''), + caption: String(slide?.caption ?? ''), }); /** @@ -323,8 +321,8 @@ export const mergeElementWithDefaults = ( : Array.isArray(defaults.galleryCards) ? defaults.galleryCards : element.galleryCards || []; - merged.galleryCards = cards.map((card, i) => - normalizeGalleryCard(card as unknown as Record, i), + merged.galleryCards = cards.map((card) => + normalizeGalleryCard(card as unknown as Record), ); // Handle gallery info spans array @@ -349,8 +347,8 @@ export const mergeElementWithDefaults = ( : Array.isArray(defaults.carouselSlides) ? defaults.carouselSlides : element.carouselSlides || []; - merged.carouselSlides = slides.map((slide, i) => - normalizeCarouselSlide(slide as unknown as Record, i), + merged.carouselSlides = slides.map((slide) => + normalizeCarouselSlide(slide as unknown as Record), ); } @@ -380,8 +378,8 @@ export const parseElementSettings = ( // Parse gallery cards if present if (Array.isArray(settings.galleryCards)) { - settings.galleryCards = settings.galleryCards.map((card, i) => - normalizeGalleryCard(card as Record, i), + settings.galleryCards = settings.galleryCards.map((card) => + normalizeGalleryCard(card as Record), ); } @@ -394,8 +392,8 @@ export const parseElementSettings = ( // Parse carousel slides if present if (Array.isArray(settings.carouselSlides)) { - settings.carouselSlides = settings.carouselSlides.map((slide, i) => - normalizeCarouselSlide(slide as Record, i), + settings.carouselSlides = settings.carouselSlides.map((slide) => + normalizeCarouselSlide(slide as Record), ); } @@ -550,18 +548,18 @@ export const buildElementSettings = ( // Gallery type settings if (isGalleryElementType(elementType)) { if (Array.isArray(element.galleryCards)) { - settings.galleryCards = element.galleryCards.map((card, i) => ({ + settings.galleryCards = element.galleryCards.map((card) => ({ id: String(card.id || createLocalId()), - imageUrl: card.imageUrl || '', - title: card.title || `Card ${i + 1}`, - description: card.description || '', + imageUrl: card.imageUrl ?? '', + title: card.title ?? '', + description: card.description ?? '', })); } if (Array.isArray(element.galleryInfoSpans)) { settings.galleryInfoSpans = element.galleryInfoSpans.map((span) => ({ id: String(span.id || createLocalId()), - text: span.text || '', - iconUrl: span.iconUrl || undefined, + text: span.text ?? '', + iconUrl: span.iconUrl ?? undefined, })); } addIfNotEmpty( @@ -592,10 +590,10 @@ export const buildElementSettings = ( // Carousel type settings if (isCarouselElementType(elementType)) { if (Array.isArray(element.carouselSlides)) { - settings.carouselSlides = element.carouselSlides.map((slide, i) => ({ + settings.carouselSlides = element.carouselSlides.map((slide) => ({ id: String(slide.id || createLocalId()), - imageUrl: slide.imageUrl || '', - caption: slide.caption || `Slide ${i + 1}`, + imageUrl: slide.imageUrl ?? '', + caption: slide.caption ?? '', })); } addIfNotEmpty(settings, 'carouselPrevIconUrl', element.carouselPrevIconUrl); diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index 0664c20..62b479c 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -820,11 +820,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { item.appearDurationSec, ), galleryCards: Array.isArray(item.galleryCards) - ? item.galleryCards.map((card: any, index: number) => ({ + ? item.galleryCards.map((card: any) => ({ id: String(card?.id || createLocalId()), - imageUrl: String(card?.imageUrl || ''), - title: String(card?.title || `Card ${index + 1}`), - description: String(card?.description || ''), + imageUrl: String(card?.imageUrl ?? ''), + title: String(card?.title ?? ''), + description: String(card?.description ?? ''), })) : undefined, galleryHeaderImageUrl: @@ -838,7 +838,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { galleryInfoSpans: Array.isArray(item.galleryInfoSpans) ? item.galleryInfoSpans.map((span: any) => ({ id: String(span?.id || createLocalId()), - text: String(span?.text || ''), + text: String(span?.text ?? ''), })) : undefined, galleryColumns: @@ -846,10 +846,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { ? item.galleryColumns : undefined, carouselSlides: Array.isArray(item.carouselSlides) - ? item.carouselSlides.map((slide: any, index: number) => ({ + ? item.carouselSlides.map((slide: any) => ({ id: String(slide?.id || createLocalId()), - imageUrl: String(slide?.imageUrl || ''), - caption: String(slide?.caption || `Slide ${index + 1}`), + imageUrl: String(slide?.imageUrl ?? ''), + caption: String(slide?.caption ?? ''), })) : undefined, iconUrl: typeof item.iconUrl === 'string' ? item.iconUrl : '',