From 42684051c3ce46c54579b76003f58c04798bb540 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Mar 2026 15:26:24 +0000 Subject: [PATCH] Basic constructor --- frontend/src/layouts/Authenticated.tsx | 67 ++++++----- frontend/src/pages/constructor.tsx | 158 ++++++++++++++++++++----- 2 files changed, 165 insertions(+), 60 deletions(-) diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index f974115..60619c7 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -86,6 +86,7 @@ export default function LayoutAuthenticated({ const darkMode = useAppSelector((state) => state.style.darkMode) + const isConstructorFullscreen = router.pathname === '/constructor' const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false) const [isAsideLgActive, setIsAsideLgActive] = useState(false) @@ -106,43 +107,47 @@ export default function LayoutAuthenticated({ }, [router.events, dispatch]) - const layoutAsidePadding = 'lg:pl-60' + const layoutAsidePadding = isConstructorFullscreen ? '' : 'lg:pl-60' + const layoutTopPadding = isConstructorFullscreen ? '' : 'pt-14' + const mobileAsideShift = !isConstructorFullscreen && isAsideMobileExpanded ? 'ml-60 lg:ml-0' : '' return (
- - setIsAsideMobileExpanded(!isAsideMobileExpanded)} - > - - - setIsAsideLgActive(true)} - > - - - - - - - setIsAsideLgActive(false)} - /> + {!isConstructorFullscreen && ( + <> + + setIsAsideMobileExpanded(!isAsideMobileExpanded)} + > + + + setIsAsideLgActive(true)} + > + + + + + + + setIsAsideLgActive(false)} + /> + + )} {children} - Hand-crafted & Made with ❤️ + {!isConstructorFullscreen && Hand-crafted & Made with ❤️}
) diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index 7da6d9e..e23a675 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -15,7 +15,7 @@ import { useRouter } from 'next/router'; import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import BaseButton from '../components/BaseButton'; import BaseIcon from '../components/BaseIcon'; -import { getPageTitle } from '../config'; +import { baseURLApi, getPageTitle } from '../config'; import LayoutAuthenticated from '../layouts/Authenticated'; type TourPage = { @@ -36,6 +36,7 @@ type ProjectAsset = { name?: string; asset_type?: 'image' | 'video' | 'audio' | 'file'; cdn_url?: string | null; + storage_key?: string | null; }; type AssetOption = { @@ -134,7 +135,79 @@ const createLocalId = () => { const getAssetLabel = (asset: ProjectAsset) => { const baseName = asset.name?.trim() || 'Untitled asset'; - return `${baseName}${asset.cdn_url ? ` · ${asset.cdn_url}` : ''}`; + const source = String(asset.storage_key || asset.cdn_url || '').trim(); + return `${baseName}${source ? ` · ${source}` : ''}`; +}; + +const getAssetSourceValue = (asset: ProjectAsset) => String(asset.storage_key || asset.cdn_url || '').trim(); + +const extractPrivateUrlFromDownloadPath = (value: string) => { + const normalized = String(value || '').trim(); + if (!normalized) return ''; + + try { + const parsed = new URL(normalized, 'http://localhost'); + const privateUrl = parsed.searchParams.get('privateUrl'); + return String(privateUrl || '').trim(); + } catch (error) { + console.error('Failed to parse download URL:', error); + return ''; + } +}; + +const extractS3ObjectKey = (value: string) => { + const normalized = String(value || '').trim(); + if (!normalized) return ''; + + try { + const parsed = new URL(normalized); + const hostname = String(parsed.hostname || '').toLowerCase(); + if (!hostname.includes('amazonaws.com') && !hostname.includes('cloudfront.net')) return ''; + const decodedPath = decodeURIComponent(String(parsed.pathname || '').replace(/^\/+/, '')); + if (!decodedPath) return ''; + + const pathParts = decodedPath.split('/').filter(Boolean); + if (pathParts.length <= 1) return decodedPath; + + const firstPart = pathParts[0]; + const isLikelyStoragePrefix = /^[a-f0-9]{24,64}$/i.test(firstPart); + if (isLikelyStoragePrefix) { + return pathParts.slice(1).join('/'); + } + + return decodedPath; + } catch (error) { + console.error('Failed to parse S3 asset URL:', error); + return ''; + } +}; + +const resolveAssetPlaybackUrl = (value?: string) => { + const normalized = String(value || '').trim(); + if (!normalized) return ''; + + if (normalized.startsWith('data:') || normalized.startsWith('blob:')) return normalized; + + if (normalized.startsWith('/api/file/download')) return normalized; + + if (normalized.startsWith('/file/download')) return `${baseURLApi}${normalized}`; + + if (normalized.startsWith('http://') || normalized.startsWith('https://')) { + const downloadPrivateUrl = extractPrivateUrlFromDownloadPath(normalized); + if (downloadPrivateUrl) { + return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(downloadPrivateUrl)}`; + } + + const s3ObjectKey = extractS3ObjectKey(normalized); + if (s3ObjectKey) { + return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(s3ObjectKey)}`; + } + + return normalized; + } + + const normalizedPrivateUrl = normalized.replace(/^\/+/, ''); + return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPrivateUrl)}`; }; const isBackgroundImageAsset = (asset: ProjectAsset) => { @@ -295,6 +368,7 @@ const ConstructorPage = () => { const elementDragRef = useRef(null); const transitionVideoRef = useRef(null); const reverseAnimationFrame = useRef(null); + const didSetInitialCanvasFocus = useRef(false); const activePage = useMemo(() => pages.find((item) => item.id === activePageId) || null, [activePageId, pages]); const pageNameById = useMemo(() => { @@ -311,35 +385,37 @@ const ConstructorPage = () => { const imageAssetOptions = useMemo( () => assets - .filter((asset) => asset.asset_type === 'image' && asset.cdn_url) - .map((asset) => ({ value: String(asset.cdn_url || ''), label: getAssetLabel(asset) })), + .filter((asset) => asset.asset_type === 'image' && getAssetSourceValue(asset)) + .map((asset) => ({ value: getAssetSourceValue(asset), 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) })), + .filter((asset) => asset.asset_type === 'image' && getAssetSourceValue(asset) && isBackgroundImageAsset(asset)) + .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) })), [assets], ); const videoAssetOptions = useMemo( () => assets - .filter((asset) => asset.asset_type === 'video' && asset.cdn_url) - .map((asset) => ({ value: String(asset.cdn_url || ''), label: getAssetLabel(asset) })), + .filter((asset) => asset.asset_type === 'video' && getAssetSourceValue(asset)) + .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) })), [assets], ); const audioAssetOptions = useMemo( () => assets - .filter((asset) => asset.asset_type === 'audio' && asset.cdn_url) - .map((asset) => ({ value: String(asset.cdn_url || ''), label: getAssetLabel(asset) })), + .filter((asset) => asset.asset_type === 'audio' && getAssetSourceValue(asset)) + .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) })), [assets], ); const transitionVideoAssetOptions = useMemo(() => { const tagged = assets - .filter((asset) => asset.asset_type === 'video' && asset.cdn_url && /\[TRANSITION\]/i.test(String(asset.name || ''))) - .map((asset) => ({ value: String(asset.cdn_url || ''), label: getAssetLabel(asset) })); + .filter( + (asset) => asset.asset_type === 'video' && getAssetSourceValue(asset) && /\[TRANSITION\]/i.test(String(asset.name || '')), + ) + .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) })); if (tagged.length > 0) return tagged; @@ -374,7 +450,7 @@ const ConstructorPage = () => { const defaultPageId = pageIdFromRoute || pageRows[0]?.id || ''; setActivePageId(defaultPageId); - setIsMenuOpen(pageRows.length > 0); + setIsMenuOpen(false); } catch (error: any) { if (error?.response?.status === 401) { const message = 'Your session has expired. Please sign in again.'; @@ -440,6 +516,17 @@ const ConstructorPage = () => { }); }, []); + useEffect(() => { + if (!router.isReady || !isAuthReady || isLoading) return; + if (didSetInitialCanvasFocus.current) return; + if (!canvasRef.current) return; + + didSetInitialCanvasFocus.current = true; + requestAnimationFrame(() => { + canvasRef.current?.focus({ preventScroll: true }); + }); + }, [isAuthReady, isLoading, router.isReady]); + useEffect(() => { if (!activePage) { setElements([]); @@ -498,7 +585,7 @@ const ConstructorPage = () => { setSelectedElementId((current) => { if (!normalizedElements.length) return ''; if (normalizedElements.some((element) => element.id === current)) return current; - return normalizedElements[0].id; + return ''; }); setBackgroundImageUrl(activePage.background_image_url || ''); setBackgroundVideoUrl(activePage.background_video_url || ''); @@ -890,7 +977,11 @@ const ConstructorPage = () => {
{card.imageUrl ? ( // eslint-disable-next-line @next/next/no-img-element - {card.title + {card.title ) : (
No image
)} @@ -909,7 +1000,11 @@ const ConstructorPage = () => {
{firstSlide?.imageUrl ? ( // eslint-disable-next-line @next/next/no-img-element - {firstSlide.caption + {firstSlide.caption ) : (
No slide image
)} @@ -926,7 +1021,7 @@ const ConstructorPage = () => {
-
- {backgroundImageUrl ? ( +
+ {backgroundImageSrc ? ( // eslint-disable-next-line @next/next/no-img-element Background ) : null} - {backgroundVideoUrl ? ( + {backgroundVideoSrc ? (