From 8a20fdbd9e1eb062db4e138b90f08c11fe0027c4 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Mar 2026 14:56:05 +0000 Subject: [PATCH] Autosave: 20260317-145604 --- frontend/src/pages/constructor.tsx | 195 +++++++++++++++++++++++++++-- 1 file changed, 185 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index 6e56509..7da6d9e 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -16,6 +16,7 @@ import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState import BaseButton from '../components/BaseButton'; import BaseIcon from '../components/BaseIcon'; import { getPageTitle } from '../config'; +import LayoutAuthenticated from '../layouts/Authenticated'; type TourPage = { id: string; @@ -136,6 +137,14 @@ const getAssetLabel = (asset: ProjectAsset) => { return `${baseName}${asset.cdn_url ? ` · ${asset.cdn_url}` : ''}`; }; +const isBackgroundImageAsset = (asset: ProjectAsset) => { + const normalizedName = String(asset.name || '').toLowerCase(); + if (!normalizedName) return false; + const hasBackgroundKeyword = /\bbackground\b|\bbg\b|backdrop|wallpaper/.test(normalizedName); + const hasExcludedKeyword = /\bicon\b|\blogo\b/.test(normalizedName); + return hasBackgroundKeyword && !hasExcludedKeyword; +}; + const addFallbackAssetOption = (options: AssetOption[], value?: string, fallbackLabel?: string): AssetOption[] => { const normalizedValue = String(value || '').trim(); if (!normalizedValue) return options; @@ -238,6 +247,7 @@ const ConstructorPage = () => { const router = useRouter(); const canvasRef = useRef(null); const elementEditorRef = useRef(null); + const [isAuthReady, setIsAuthReady] = useState(false); const projectId = useMemo(() => { const value = router.query.projectId; @@ -287,6 +297,13 @@ const ConstructorPage = () => { const reverseAnimationFrame = useRef(null); const activePage = useMemo(() => pages.find((item) => item.id === activePageId) || null, [activePageId, pages]); + const pageNameById = useMemo(() => { + const acc: Record = {}; + pages.forEach((page, index) => { + acc[String(page.id)] = page.name || `Page ${index + 1}`; + }); + return acc; + }, [pages]); const selectedElement = useMemo( () => elements.find((element) => element.id === selectedElementId) || null, [elements, selectedElementId], @@ -298,6 +315,13 @@ const ConstructorPage = () => { .map((asset) => ({ value: String(asset.cdn_url || ''), label: getAssetLabel(asset) })), [assets], ); + const backgroundImageAssetOptions = useMemo( + () => + assets + .filter((asset) => asset.asset_type === 'image' && asset.cdn_url && isBackgroundImageAsset(asset)) + .map((asset) => ({ value: String(asset.cdn_url || ''), label: getAssetLabel(asset) })), + [assets], + ); const videoAssetOptions = useMemo( () => assets @@ -329,7 +353,7 @@ const ConstructorPage = () => { }, [newTransitionVideoUrl, transitionVideoAssetOptions]); const loadData = useCallback(async () => { - if (!projectId) return; + if (!projectId || !router.isReady || !isAuthReady) return; try { setIsLoading(true); @@ -352,6 +376,16 @@ const ConstructorPage = () => { setActivePageId(defaultPageId); setIsMenuOpen(pageRows.length > 0); } catch (error: any) { + if (error?.response?.status === 401) { + const message = 'Your session has expired. Please sign in again.'; + console.error('Unauthorized constructor request:', error); + setErrorMessage(message); + setPages([]); + setAssets([]); + router.replace('/login'); + return; + } + const message = error?.response?.data?.message || error?.message || 'Failed to load constructor data.'; console.error('Failed to load constructor data:', error); setErrorMessage(message); @@ -360,7 +394,21 @@ const ConstructorPage = () => { } finally { setIsLoading(false); } - }, [pageIdFromRoute, projectId]); + }, [isAuthReady, pageIdFromRoute, projectId, router]); + + useEffect(() => { + if (!router.isReady || typeof window === 'undefined') return; + + const token = sessionStorage.getItem('token') || localStorage.getItem('token'); + if (!token) { + setIsAuthReady(false); + setErrorMessage('Please sign in to continue.'); + router.replace('/login'); + return; + } + + setIsAuthReady(true); + }, [router]); useEffect(() => { if (!router.isReady) return; @@ -803,8 +851,117 @@ const ConstructorPage = () => { }; }; + const renderCanvasElementContent = (element: CanvasElement) => { + if (element.type === 'navigation_next' || element.type === 'navigation_prev') { + const targetPageName = element.targetPageId ? pageNameById[element.targetPageId] : ''; + return ( +
+ {element.navLabel || (element.type === 'navigation_next' ? 'Forward' : 'Back')} + {targetPageName ? To: {targetPageName} : null} +
+ ); + } + + if (element.type === 'tooltip') { + return ( +
+

{element.tooltipTitle || 'Tooltip title'}

+

{element.tooltipText || 'Tooltip text'}

+
+ ); + } + + if (element.type === 'description') { + return ( +
+

{element.descriptionTitle || 'Description title'}

+

{element.descriptionText || 'Description text'}

+
+ ); + } + + if (element.type === 'gallery') { + const cards = element.galleryCards || []; + return ( +
+

Gallery ({cards.length})

+
+ {cards.slice(0, 6).map((card) => ( +
+ {card.imageUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {card.title + ) : ( +
No image
+ )} +
+ ))} +
+
+ ); + } + + if (element.type === 'carousel') { + const firstSlide = (element.carouselSlides || [])[0]; + return ( +
+

Carousel ({element.carouselSlides?.length || 0})

+
+ {firstSlide?.imageUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {firstSlide.caption + ) : ( +
No slide image
+ )} +
+

{firstSlide?.caption || 'No caption'}

+
+ ); + } + + if (element.type === 'video_player') { + return ( +
+

Video player

+
+ ); + } + + if (element.type === 'audio_player') { + return ( +
+

Audio player

+
+ ); + } + + return getElementButtonTitle(element); + }; + const canvasBackgroundStyle: React.CSSProperties = {}; - const backgroundImageSelectOptions = addFallbackAssetOption(imageAssetOptions, backgroundImageUrl, `Current image · ${backgroundImageUrl}`); + const backgroundImageSelectOptions = addFallbackAssetOption( + backgroundImageAssetOptions, + backgroundImageUrl, + `Current image · ${backgroundImageUrl}`, + ); const backgroundVideoSelectOptions = addFallbackAssetOption(videoAssetOptions, backgroundVideoUrl, `Current video · ${backgroundVideoUrl}`); const backgroundAudioSelectOptions = addFallbackAssetOption(audioAssetOptions, backgroundAudioUrl, `Current audio · ${backgroundAudioUrl}`); const hasEditorSelection = Boolean(selectedElement) || selectedMenuItem !== 'none'; @@ -818,7 +975,7 @@ const ConstructorPage = () => { : selectedElement?.label || 'Element editor'; if (backgroundImageUrl) { - canvasBackgroundStyle.backgroundImage = `url(${backgroundImageUrl})`; + canvasBackgroundStyle.backgroundImage = `url("${backgroundImageUrl}")`; canvasBackgroundStyle.backgroundSize = 'cover'; canvasBackgroundStyle.backgroundPosition = 'center'; } @@ -939,11 +1096,29 @@ const ConstructorPage = () => {
- {backgroundVideoUrl ? ( -