updated page dropdown in the constructor

This commit is contained in:
Dmitri 2026-05-08 16:44:25 +02:00
parent 06a29dbf6a
commit 027af5082b
6 changed files with 418 additions and 95 deletions

View File

@ -0,0 +1,123 @@
/**
* CreatePageModal Component
*
* Modal dialog for creating new pages with custom name.
* Slug is auto-generated from the page name behind the scenes.
*/
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import CardBoxModal from '../CardBoxModal';
import { sanitizeSlug, buildUniqueSlug } from '../../lib/slugHelpers';
interface CreatePageModalProps {
/** Whether the modal is visible */
isActive: boolean;
/** Whether page creation is in progress */
isCreating: boolean;
/** Set of existing slugs for uniqueness validation */
existingSlugs: Set<string>;
/** Suggested page number for default name */
suggestedPageNumber: number;
/** Called when user confirms with valid name and slug */
onConfirm: (pageName: string, slug: string) => void;
/** Called when user cancels */
onCancel: () => void;
}
/**
* Modal for creating new pages - only asks for name, slug is auto-generated
*/
const CreatePageModal: React.FC<CreatePageModalProps> = ({
isActive,
isCreating,
existingSlugs,
suggestedPageNumber,
onConfirm,
onCancel,
}) => {
const [pageName, setPageName] = useState('');
const [nameError, setNameError] = useState('');
// Reset form when modal opens
useEffect(() => {
if (isActive) {
const defaultName = `Page ${suggestedPageNumber}`;
setPageName(defaultName);
setNameError('');
}
}, [isActive, suggestedPageNumber]);
// Validate name
const nameValidationError = useMemo(() => {
const trimmed = pageName.trim();
if (!trimmed) return 'Page name is required';
if (trimmed.length > 255) return 'Page name must be 255 characters or less';
return '';
}, [pageName]);
// Auto-generate unique slug from name
const generatedSlug = useMemo(() => {
const baseSlug = sanitizeSlug(pageName) || 'page';
return buildUniqueSlug(baseSlug, existingSlugs);
}, [pageName, existingSlugs]);
const handleNameChange = useCallback((value: string) => {
setPageName(value);
setNameError('');
}, []);
const handleConfirm = useCallback(() => {
if (nameValidationError) {
setNameError(nameValidationError);
return;
}
onConfirm(pageName.trim(), generatedSlug);
}, [pageName, generatedSlug, nameValidationError, onConfirm]);
const handleCancel = useCallback(() => {
if (!isCreating) {
onCancel();
}
}, [isCreating, onCancel]);
const isConfirmDisabled = isCreating || Boolean(nameValidationError);
return (
<CardBoxModal
title="Create page"
buttonColor="info"
buttonLabel={isCreating ? 'Creating...' : 'Create'}
isConfirmDisabled={isConfirmDisabled}
isActive={isActive}
onConfirm={handleConfirm}
onCancel={isCreating ? undefined : handleCancel}
>
<div>
<label
htmlFor="create-page-name"
className="block text-sm font-semibold mb-1"
>
Page name
</label>
<input
id="create-page-name"
type="text"
value={pageName}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="Enter page name"
className="w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800"
autoFocus
maxLength={255}
/>
{(nameError || nameValidationError) && (
<p className="text-xs text-red-600 mt-1">
{nameError || nameValidationError}
</p>
)}
</div>
</CardBoxModal>
);
};
export default CreatePageModal;

View File

@ -6,6 +6,7 @@ import {
mdiViewDashboard, mdiViewDashboard,
mdiChevronDown, mdiChevronDown,
mdiChevronUp, mdiChevronUp,
mdiPencil,
} from '@mdi/js'; } from '@mdi/js';
import Icon from '@mdi/react'; import Icon from '@mdi/react';
import axios from 'axios'; import axios from 'axios';
@ -22,7 +23,7 @@ import { getPageTitle } from '../config';
import { hasPermission } from '../helpers/userPermissions'; import { hasPermission } from '../helpers/userPermissions';
import { useAppSelector, useAppDispatch } from '../stores/hooks'; import { useAppSelector, useAppDispatch } from '../stores/hooks';
import { logger } from '../lib/logger'; import { logger } from '../lib/logger';
import { sanitizeSlug, buildUniqueSlug, slugPattern } from '../lib/slugHelpers'; import { sanitizeSlug, buildUniqueSlug } from '../lib/slugHelpers';
import { import {
toRoutePath, toRoutePath,
compareRoutes, compareRoutes,
@ -114,10 +115,15 @@ const TourFlowManager = () => {
const [isCreatingPage, setIsCreatingPage] = useState(false); const [isCreatingPage, setIsCreatingPage] = useState(false);
const [isCreatingTransition, setIsCreatingTransition] = useState(false); const [isCreatingTransition, setIsCreatingTransition] = useState(false);
const [isCreatePageModalActive, setIsCreatePageModalActive] = useState(false); const [isCreatePageModalActive, setIsCreatePageModalActive] = useState(false);
const [newPageSlug, setNewPageSlug] = useState(''); const [newPageName, setNewPageName] = useState('');
const [newPageSlugError, setNewPageSlugError] = useState('');
const [deletingId, setDeletingId] = useState(''); const [deletingId, setDeletingId] = useState('');
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
// Edit page modal state
const [isEditPageModalActive, setIsEditPageModalActive] = useState(false);
const [editingPageId, setEditingPageId] = useState('');
const [editPageName, setEditPageName] = useState('');
const [editPageNameError, setEditPageNameError] = useState('');
const [isSavingPageName, setIsSavingPageName] = useState(false);
// Use selector for current project's dev transition settings // Use selector for current project's dev transition settings
const projectTransitionSettingsEntity = useAppSelector((state) => const projectTransitionSettingsEntity = useAppSelector((state) =>
@ -337,15 +343,25 @@ const TourFlowManager = () => {
[pages, targetEnvironment], [pages, targetEnvironment],
); );
const slugValidationError = useMemo(() => { const nameValidationError = useMemo(() => {
const slug = newPageSlug.trim(); const name = newPageName.trim();
if (!slug) return 'Slug is required.'; if (!name) return 'Page name is required.';
if (!slugPattern.test(slug)) if (name.length > 255) return 'Page name must be 255 characters or less.';
return 'Use lowercase letters, numbers, and hyphens only.';
if (pageSlugsInEnvironment.has(slug))
return 'This slug already exists in the selected environment.';
return ''; return '';
}, [newPageSlug, pageSlugsInEnvironment]); }, [newPageName]);
// Auto-generate unique slug from name
const generatedSlug = useMemo(() => {
const baseSlug = sanitizeSlug(newPageName) || 'page';
return buildUniqueSlug(baseSlug, pageSlugsInEnvironment);
}, [newPageName, pageSlugsInEnvironment]);
const editNameValidationError = useMemo(() => {
const name = editPageName.trim();
if (!name) return 'Page name is required.';
if (name.length > 255) return 'Page name must be 255 characters or less.';
return '';
}, [editPageName]);
const nextPageNumber = useMemo(() => pages.length + 1, [pages.length]); const nextPageNumber = useMemo(() => pages.length + 1, [pages.length]);
@ -407,24 +423,113 @@ const TourFlowManager = () => {
return; return;
} }
const suggestedSlug = buildUniqueSlug( const suggestedName = `Page ${nextPageNumber}`;
`page-${nextPageNumber}`, setNewPageName(suggestedName);
pageSlugsInEnvironment,
);
setNewPageSlug(suggestedSlug);
setNewPageSlugError('');
setIsCreatePageModalActive(true); setIsCreatePageModalActive(true);
}; };
const handleSlugChange = (value: string) => { const handleNameChange = (value: string) => {
setNewPageSlug(value); setNewPageName(value);
setNewPageSlugError('');
}; };
const closeCreatePageModal = () => { const closeCreatePageModal = () => {
setIsCreatePageModalActive(false); setIsCreatePageModalActive(false);
setNewPageSlug(''); setNewPageName('');
setNewPageSlugError(''); };
// Edit page modal handlers
const openEditPageModal = (event: React.MouseEvent, page: TourPage) => {
event.stopPropagation();
setEditingPageId(page.id);
setEditPageName(page.name || '');
setEditPageNameError('');
setIsEditPageModalActive(true);
};
const handleEditNameChange = (value: string) => {
setEditPageName(value);
setEditPageNameError('');
};
const closeEditPageModal = () => {
setIsEditPageModalActive(false);
setEditingPageId('');
setEditPageName('');
setEditPageNameError('');
};
const handleSavePageName = async () => {
if (editNameValidationError) {
setEditPageNameError(editNameValidationError);
return;
}
try {
setIsSavingPageName(true);
setEditPageNameError('');
// Fetch full page data from API to preserve all fields
// The backend's getFieldMapping converts missing fields to null
const pageResponse = await axios.get(`/tour_pages/${editingPageId}`);
const fullPageData = pageResponse.data;
if (!fullPageData) {
setEditPageNameError('Page not found');
return;
}
// Send all existing fields with only name changed
await axios.put(`/tour_pages/${editingPageId}`, {
id: editingPageId,
data: {
environment: fullPageData.environment,
source_key: fullPageData.source_key,
name: editPageName.trim(),
slug: fullPageData.slug,
sort_order: fullPageData.sort_order,
background_image_url: fullPageData.background_image_url,
background_video_url: fullPageData.background_video_url,
background_audio_url: fullPageData.background_audio_url,
background_loop: fullPageData.background_loop,
background_video_autoplay: fullPageData.background_video_autoplay,
background_video_loop: fullPageData.background_video_loop,
background_video_muted: fullPageData.background_video_muted,
background_video_start_time: fullPageData.background_video_start_time,
background_video_end_time: fullPageData.background_video_end_time,
design_width: fullPageData.design_width,
design_height: fullPageData.design_height,
requires_auth: fullPageData.requires_auth,
ui_schema_json: fullPageData.ui_schema_json,
},
});
// Update local state
setPages((prev) =>
prev.map((page) =>
page.id === editingPageId
? { ...page, name: editPageName.trim() }
: page,
),
);
closeEditPageModal();
toast.success('Page name updated');
} catch (error: unknown) {
const axiosError = error as {
response?: { data?: { message?: string } };
};
const message =
axiosError?.response?.data?.message ||
(error instanceof Error ? error.message : null) ||
'Failed to update page name.';
setEditPageNameError(message);
logger.error(
'Failed to update page name:',
error instanceof Error ? error : { error },
);
} finally {
setIsSavingPageName(false);
}
}; };
const handleCreatePage = async () => { const handleCreatePage = async () => {
@ -433,23 +538,22 @@ const TourFlowManager = () => {
return; return;
} }
const slug = newPageSlug.trim(); const name = newPageName.trim();
const validationError = slugValidationError || newPageSlugError;
if (validationError) { // Validate name
setNewPageSlugError(validationError); if (nameValidationError) {
return; return;
} }
try { try {
setIsCreatingPage(true); setIsCreatingPage(true);
setErrorMessage(''); setErrorMessage('');
setNewPageSlugError('');
const payload = { const payload = {
project: activeProjectId, project: activeProjectId,
environment: targetEnvironment, environment: targetEnvironment,
source_key: '', source_key: '',
name: `Page ${nextPageNumber}`, name,
slug, slug: generatedSlug,
sort_order: sort_order:
Math.max(...pages.map((item) => Number(item.sort_order || 0)), 0) + 1, Math.max(...pages.map((item) => Number(item.sort_order || 0)), 0) + 1,
background_image_url: '', background_image_url: '',
@ -461,8 +565,7 @@ const TourFlowManager = () => {
}; };
await axios.post('/tour_pages', { data: payload }); await axios.post('/tour_pages', { data: payload });
setIsCreatePageModalActive(false); closeCreatePageModal();
setNewPageSlug('');
await loadData(); await loadData();
} catch (error: unknown) { } catch (error: unknown) {
const axiosError = error as { const axiosError = error as {
@ -473,7 +576,6 @@ const TourFlowManager = () => {
(error instanceof Error ? error.message : null) || (error instanceof Error ? error.message : null) ||
'Failed to create page.'; 'Failed to create page.';
setErrorMessage(message); setErrorMessage(message);
setNewPageSlugError(message);
logger.error( logger.error(
'Failed to create page:', 'Failed to create page:',
error instanceof Error ? error : { error }, error instanceof Error ? error : { error },
@ -776,33 +878,66 @@ const TourFlowManager = () => {
title='Create page' title='Create page'
buttonColor='info' buttonColor='info'
buttonLabel={isCreatingPage ? 'Creating...' : 'Create'} buttonLabel={isCreatingPage ? 'Creating...' : 'Create'}
isConfirmDisabled={Boolean(slugValidationError) || isCreatingPage} isConfirmDisabled={Boolean(nameValidationError) || isCreatingPage}
isActive={isCreatePageModalActive} isActive={isCreatePageModalActive}
onConfirm={handleCreatePage} onConfirm={handleCreatePage}
onCancel={isCreatingPage ? undefined : closeCreatePageModal} onCancel={isCreatingPage ? undefined : closeCreatePageModal}
> >
<div> <div>
<label <label
htmlFor='new-page-slug' htmlFor='new-page-name'
className='block text-sm font-semibold mb-1' className='block text-sm font-semibold mb-1'
> >
Page slug Page name
</label> </label>
<input <input
id='new-page-slug' id='new-page-name'
type='text' type='text'
value={newPageSlug} value={newPageName}
onChange={(event) => handleSlugChange(event.target.value)} onChange={(event) => handleNameChange(event.target.value)}
placeholder='my-page-slug' placeholder='Enter page name'
className='w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800' className='w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800'
autoFocus autoFocus
maxLength={255}
/> />
<p className='text-xs text-gray-500 mt-2'> {nameValidationError && (
Use lowercase letters, numbers, and hyphens. <p className='text-xs text-red-600 mt-1'>
{nameValidationError}
</p> </p>
{(newPageSlugError || slugValidationError) && ( )}
<p className='text-xs text-red-600 mt-2'> </div>
{newPageSlugError || slugValidationError} </CardBoxModal>
{/* Edit Page Name Modal */}
<CardBoxModal
title='Edit page name'
buttonColor='info'
buttonLabel={isSavingPageName ? 'Saving...' : 'Save'}
isConfirmDisabled={Boolean(editNameValidationError) || isSavingPageName}
isActive={isEditPageModalActive}
onConfirm={handleSavePageName}
onCancel={isSavingPageName ? undefined : closeEditPageModal}
>
<div>
<label
htmlFor='edit-page-name'
className='block text-sm font-semibold mb-1'
>
Page name
</label>
<input
id='edit-page-name'
type='text'
value={editPageName}
onChange={(event) => handleEditNameChange(event.target.value)}
placeholder='Enter page name'
className='w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800'
autoFocus
maxLength={255}
/>
{(editPageNameError || editNameValidationError) && (
<p className='text-xs text-red-600 mt-1'>
{editPageNameError || editNameValidationError}
</p> </p>
)} )}
</div> </div>
@ -829,6 +964,9 @@ const TourFlowManager = () => {
const canDelete = const canDelete =
entry.type === 'page' ? canDeletePage : canDeleteTransition; entry.type === 'page' ? canDeletePage : canDeleteTransition;
const isDeleting = deletingId === entry.id; const isDeleting = deletingId === entry.id;
const pageData = entry.type === 'page'
? pages.find((p) => p.id === entry.id)
: null;
return ( return (
<li key={`${entry.type}-${entry.id}`}> <li key={`${entry.type}-${entry.id}`}>
@ -854,7 +992,7 @@ const TourFlowManager = () => {
} }
}} }}
> >
<div className='pr-8'> <div className='pr-20'>
<p className='text-xs uppercase text-gray-500 mb-1'> <p className='text-xs uppercase text-gray-500 mb-1'>
{entry.description} {entry.description}
</p> </p>
@ -864,8 +1002,19 @@ const TourFlowManager = () => {
</p> </p>
</div> </div>
<div className='absolute top-3 right-3 flex gap-1'>
{entry.type === 'page' && pageData && (
<BaseButton
icon={mdiPencil}
color='info'
outline
small
onClick={(event) =>
openEditPageModal(event, pageData)
}
/>
)}
<BaseButton <BaseButton
className='!absolute top-3 right-3'
icon={mdiClose} icon={mdiClose}
color='danger' color='danger'
outline outline
@ -876,6 +1025,7 @@ const TourFlowManager = () => {
disabled={!canDelete || isDeleting} disabled={!canDelete || isDeleting}
/> />
</div> </div>
</div>
</li> </li>
); );
})} })}

View File

@ -105,9 +105,16 @@ export function useElementWrapperStyle({
// Build inline style from element properties // Build inline style from element properties
const inlineStyle = buildElementStyle(element); const inlineStyle = buildElementStyle(element);
// If element has configured padding, use it exclusively (skip default paddingStyle)
// This avoids React warning about conflicting shorthand/non-shorthand properties
const finalStyle =
'padding' in inlineStyle
? inlineStyle
: { ...paddingStyle, ...inlineStyle };
return { return {
className: classNames, className: classNames,
style: { ...paddingStyle, ...inlineStyle }, style: finalStyle,
}; };
}, [element, isSelected, isEditMode]); }, [element, isSelected, isEditMode]);
} }

View File

@ -80,8 +80,8 @@ interface UseConstructorPageActionsResult {
saveConstructor: () => Promise<void>; saveConstructor: () => Promise<void>;
/** Save dev content to stage environment */ /** Save dev content to stage environment */
saveToStage: () => Promise<void>; saveToStage: () => Promise<void>;
/** Create a new page */ /** Create a new page with the given name and slug */
createPage: () => Promise<void>; createPage: (pageName: string, slug: string) => Promise<void>;
/** Create a transition (legacy - transitions are now stored on elements) */ /** Create a transition (legacy - transitions are now stored on elements) */
createTransition: (params: { createTransition: (params: {
name?: string; name?: string;
@ -256,24 +256,33 @@ export function useConstructorPageActions({
} }
}, [projectId, saveConstructor, onError, onSuccess]); }, [projectId, saveConstructor, onError, onSuccess]);
const createPage = useCallback(async () => { const createPage = useCallback(async (pageName: string, slug: string) => {
if (!projectId) { if (!projectId) {
onError?.('Project is required.'); onError?.('Project is required.');
return; return;
} }
if (!pageName.trim()) {
onError?.('Page name is required.');
return;
}
if (!slug.trim()) {
onError?.('Page slug is required.');
return;
}
const maxSortOrder = Math.max( const maxSortOrder = Math.max(
0, 0,
...pages.map((item) => Number(item.sort_order || 0)), ...pages.map((item) => Number(item.sort_order || 0)),
); );
const nextPageNumber = pages.length + 1;
const payload = { const payload = {
project: projectId, project: projectId,
environment: activePage?.environment || 'dev', environment: activePage?.environment || 'dev',
source_key: '', source_key: '',
name: `Page ${nextPageNumber}`, name: pageName.trim(),
slug: `page-${nextPageNumber}-${Date.now().toString().slice(-4)}`, slug: slug.trim(),
sort_order: maxSortOrder + 1, sort_order: maxSortOrder + 1,
background_image_url: '', background_image_url: '',
background_video_url: '', background_video_url: '',

View File

@ -47,17 +47,14 @@ export function sanitizeSlug(value: string): string {
*/ */
export function buildUniqueSlug( export function buildUniqueSlug(
baseValue: string, baseValue: string,
usedSlugs: Set<string>, _usedSlugs: Set<string>,
): string { ): string {
const baseSlug = sanitizeSlug(baseValue) || 'page'; const baseSlug = sanitizeSlug(baseValue) || 'page';
if (!usedSlugs.has(baseSlug)) return baseSlug;
let suffix = 2; // Always append timestamp to guarantee uniqueness
while (usedSlugs.has(`${baseSlug}-${suffix}`)) { // (_usedSlugs kept for API compatibility but not used - cached data may be stale)
suffix += 1; const timestamp = Date.now().toString(36);
} return `${baseSlug}-${timestamp}`;
return `${baseSlug}-${suffix}`;
} }
/** /**

View File

@ -19,6 +19,7 @@ import TransitionPreviewOverlay from '../components/Constructor/TransitionPrevie
import CanvasElementComponent from '../components/Constructor/CanvasElement'; import CanvasElementComponent from '../components/Constructor/CanvasElement';
import GalleryCarouselOverlay from '../components/UiElements/GalleryCarouselOverlay'; import GalleryCarouselOverlay from '../components/UiElements/GalleryCarouselOverlay';
import ElementEditorPanel from '../components/Constructor/ElementEditorPanel'; import ElementEditorPanel from '../components/Constructor/ElementEditorPanel';
import CreatePageModal from '../components/Constructor/CreatePageModal';
import { BackdropPortalProvider } from '../components/BackdropPortal'; import { BackdropPortalProvider } from '../components/BackdropPortal';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated'; import LayoutAuthenticated from '../layouts/Authenticated';
@ -286,6 +287,8 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
currentElementTransitionSettings, currentElementTransitionSettings,
setCurrentElementTransitionSettings, setCurrentElementTransitionSettings,
] = useState<ElementTransitionSettings | null>(null); ] = useState<ElementTransitionSettings | null>(null);
// Create page modal state
const [isCreatePageModalActive, setIsCreatePageModalActive] = useState(false);
const isConstructorEditMode = constructorInteractionMode === 'edit'; const isConstructorEditMode = constructorInteractionMode === 'edit';
const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => { const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
@ -368,6 +371,20 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
[activePageId, pages], [activePageId, pages],
); );
// Existing page slugs for uniqueness validation (current environment only)
const existingSlugs = useMemo(() => {
const targetEnv = activePage?.environment || 'dev';
return new Set(
pages
.filter((p) => (p.environment || 'dev') === targetEnv)
.map((p) => p.slug || '')
.filter(Boolean),
);
}, [pages, activePage?.environment]);
// Suggested page number for new page name
const suggestedPageNumber = useMemo(() => pages.length + 1, [pages.length]);
// Last project-level save: most recent updatedAt across all pages // Last project-level save: most recent updatedAt across all pages
const lastProjectSaveAt = useMemo(() => { const lastProjectSaveAt = useMemo(() => {
if (!pages.length) return null; if (!pages.length) return null;
@ -765,6 +782,25 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
await refreshPublishStatus(); await refreshPublishStatus();
}, [saveToStage, refreshPublishStatus]); }, [saveToStage, refreshPublishStatus]);
// Handlers for create page modal
const handleShowCreatePageModal = useCallback(async () => {
// Refresh pages data to ensure existingSlugs is up to date
await refetchData();
setIsCreatePageModalActive(true);
}, [refetchData]);
const handleCreatePageWithName = useCallback(
async (pageName: string, slug: string) => {
await createPage(pageName, slug);
setIsCreatePageModalActive(false);
},
[createPage],
);
const handleCloseCreatePageModal = useCallback(() => {
setIsCreatePageModalActive(false);
}, []);
useEffect(() => { useEffect(() => {
if (!router.isReady || typeof window === 'undefined') return; if (!router.isReady || typeof window === 'undefined') return;
@ -1077,17 +1113,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
updateBackgroundFromPage, updateBackgroundFromPage,
]); ]);
// Separate effect for initial background loading (matches RuntimePresentation pattern) // Separate effect for background loading when activePage changes.
// This effect ONLY handles initial page load when backgrounds are empty. // This handles both initial page load AND page switches (e.g., after creating a new page).
// switchToPage handles all subsequent navigation by calling navNavigateToPage directly. // The lastInitializedPageIdRef prevents redundant calls for the same page.
// Keeping this separate prevents race conditions where state updates trigger
// this effect before activePageId has been updated via applyPageSelection.
useEffect(() => { useEffect(() => {
if (!activePage || lastInitializedPageIdRef.current === activePage.id) if (!activePage || lastInitializedPageIdRef.current === activePage.id)
return; return;
// Only initialize when backgrounds are EMPTY (initial load) // Update navigation state with new page's backgrounds
if (!navCurrentBgImageUrl && !navCurrentBgVideoUrl) {
lastInitializedPageIdRef.current = activePage.id; lastInitializedPageIdRef.current = activePage.id;
navNavigateToPage({ navNavigateToPage({
id: activePage.id, id: activePage.id,
@ -1095,13 +1128,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
background_video_url: activePage.background_video_url, background_video_url: activePage.background_video_url,
background_audio_url: activePage.background_audio_url, background_audio_url: activePage.background_audio_url,
}); });
} }, [activePage, navNavigateToPage]);
}, [
activePage,
navCurrentBgImageUrl,
navCurrentBgVideoUrl,
navNavigateToPage,
]);
useEffect(() => { useEffect(() => {
if (allowedNavigationTypes.length !== 1) return; if (allowedNavigationTypes.length !== 1) return;
@ -1735,7 +1762,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
onSelectMenuItem={selectMenuItemForEdit} onSelectMenuItem={selectMenuItemForEdit}
allowedNavigationTypes={allowedNavigationTypes} allowedNavigationTypes={allowedNavigationTypes}
onAddElement={addElement} onAddElement={addElement}
onCreatePage={createPage} onCreatePage={handleShowCreatePageModal}
isCreatingPage={isCreatingPage} isCreatingPage={isCreatingPage}
onSave={saveConstructor} onSave={saveConstructor}
onSaveToStage={handleSaveToStage} onSaveToStage={handleSaveToStage}
@ -1844,7 +1871,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
isCreatingPage ? 'Creating...' : 'Create First Page' isCreatingPage ? 'Creating...' : 'Create First Page'
} }
icon={mdiPlus} icon={mdiPlus}
onClick={createPage} onClick={handleShowCreatePageModal}
disabled={isCreatingPage} disabled={isCreatingPage}
/> />
</div> </div>
@ -1962,6 +1989,16 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
/> />
)} )}
{/* Create Page Modal */}
<CreatePageModal
isActive={isCreatePageModalActive}
isCreating={isCreatingPage}
existingSlugs={existingSlugs}
suggestedPageNumber={suggestedPageNumber}
onConfirm={handleCreatePageWithName}
onCancel={handleCloseCreatePageModal}
/>
<style jsx>{` <style jsx>{`
.menu-action-btn { .menu-action-btn {
width: 100%; width: 100%;