/** * useConstructorPageActions Hook * * Handles page create/save/publish operations in the constructor. * Manages async state and API calls for page operations. */ import { useState, useCallback } from 'react'; import axios from 'axios'; import type { CanvasElement, PageBackgroundState } from '../types/constructor'; import { createLocalId } from '../lib/elementDefaults'; import { parseJsonObject } from '../lib/parseJson'; import { logger } from '../lib/logger'; interface TourPage { id: string; name?: string; slug?: string; sort_order?: number; environment?: string; source_key?: string; requires_auth?: boolean; ui_schema_json?: string; background_image_url?: string; background_video_url?: string; background_audio_url?: string; background_loop?: boolean; // Background video playback settings background_video_autoplay?: boolean; background_video_loop?: boolean; background_video_muted?: boolean; background_video_start_time?: number | null; background_video_end_time?: number | null; } interface Project { id?: string; name?: string; design_width?: number; design_height?: number; } interface UseConstructorPageActionsOptions { /** Current project ID */ projectId: string; /** Current project (for design dimensions) */ project?: Project | null; /** Array of all pages */ pages: TourPage[]; /** Currently active page */ activePage: TourPage | null; /** Current active page ID */ activePageId: string; /** Current elements array */ elements: CanvasElement[]; /** Consolidated page background state */ pageBackground: PageBackgroundState; /** Callback to reload data after operations */ onReload: (preservePageId?: string) => Promise; /** Callback to set active page ID */ onSetActivePageId: (pageId: string) => void; /** Callback to set menu open state */ onSetMenuOpen?: (isOpen: boolean) => void; /** Callback for error messages */ onError?: (message: string) => void; /** Callback for success messages */ onSuccess?: (message: string) => void; } interface UseConstructorPageActionsResult { /** Whether save operation is in progress */ isSaving: boolean; /** Whether save-to-stage operation is in progress */ isSavingToStage: boolean; /** Whether page creation is in progress */ isCreatingPage: boolean; /** Whether transition creation is in progress */ isCreatingTransition: boolean; /** Save current constructor state */ saveConstructor: () => Promise; /** Save dev content to stage environment */ saveToStage: () => Promise; /** Create a new page with the given name and slug */ createPage: (pageName: string, slug: string) => Promise; /** Create a transition (legacy - transitions are now stored on elements) */ createTransition: (params: { name?: string; videoUrl: string; supportsReverse?: boolean; durationSec?: number; }) => Promise; } /** * Hook for managing constructor page operations. * * @example * const { * isSaving, * saveConstructor, * createPage, * isCreatingPage, * } = useConstructorPageActions({ * projectId, * pages, * activePage, * activePageId, * elements, * pageBackground: background, * onReload: loadData, * onSetActivePageId: setActivePageId, * onError: setErrorMessage, * onSuccess: setSuccessMessage, * }); */ export function useConstructorPageActions({ projectId, project, pages, activePage, activePageId, elements, pageBackground, onReload, onSetActivePageId, onSetMenuOpen, onError, onSuccess, }: UseConstructorPageActionsOptions): UseConstructorPageActionsResult { // Destructure pageBackground for backward compatibility in the save logic const { imageUrl: backgroundImageUrl, videoUrl: backgroundVideoUrl, audioUrl: backgroundAudioUrl, videoSettings: { autoplay: backgroundVideoAutoplay, loop: backgroundVideoLoop, muted: backgroundVideoMuted, startTime: backgroundVideoStartTime, endTime: backgroundVideoEndTime, }, } = pageBackground; const [isSaving, setIsSaving] = useState(false); const [isSavingToStage, setIsSavingToStage] = useState(false); const [isCreatingPage, setIsCreatingPage] = useState(false); const [isCreatingTransition, setIsCreatingTransition] = useState(false); const saveConstructor = useCallback(async () => { if (!activePageId) { onError?.('Select a page before saving.'); return; } try { setIsSaving(true); const existingSchema = parseJsonObject>( activePage?.ui_schema_json, {}, ); const schemaToSave = { ...existingSchema, elements, }; await axios.put(`/tour_pages/${activePageId}`, { id: activePageId, data: { environment: activePage?.environment, source_key: activePage?.source_key, name: activePage?.name, slug: activePage?.slug, sort_order: activePage?.sort_order, requires_auth: activePage?.requires_auth, ui_schema_json: schemaToSave, background_image_url: backgroundImageUrl, background_video_url: backgroundVideoUrl, background_audio_url: backgroundAudioUrl, background_loop: Boolean(backgroundAudioUrl), background_video_autoplay: backgroundVideoAutoplay, background_video_loop: backgroundVideoLoop, background_video_muted: backgroundVideoMuted, background_video_start_time: backgroundVideoStartTime, background_video_end_time: backgroundVideoEndTime, // Copy project design dimensions to page for presentation isolation design_width: project?.design_width ?? null, design_height: project?.design_height ?? null, }, }); onSuccess?.( 'Constructor settings saved. Element positions are stored in percentages.', ); await onReload(activePageId); } 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 save constructor changes.'; logger.error( 'Failed to save constructor changes:', error instanceof Error ? error : { error }, ); onError?.(message); } finally { setIsSaving(false); } }, [ activePage?.environment, activePage?.name, activePage?.requires_auth, activePage?.slug, activePage?.sort_order, activePage?.source_key, activePage?.ui_schema_json, activePageId, pageBackground, elements, project?.design_width, project?.design_height, onError, onReload, onSuccess, ]); const saveToStage = useCallback(async () => { if (!projectId) { onError?.('Project ID is required to save to stage.'); return; } await saveConstructor(); try { setIsSavingToStage(true); await axios.post('/publish/save-to-stage', { projectId }); onSuccess?.('Saved to stage.'); } 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 save to stage.'; logger.error( 'Failed to save to stage:', error instanceof Error ? error : { error }, ); onError?.(message); } finally { setIsSavingToStage(false); } }, [projectId, saveConstructor, onError, onSuccess]); const createPage = useCallback(async (pageName: string, slug: string) => { if (!projectId) { onError?.('Project is required.'); return; } if (!pageName.trim()) { onError?.('Page name is required.'); return; } if (!slug.trim()) { onError?.('Page slug is required.'); return; } const maxSortOrder = Math.max( 0, ...pages.map((item) => Number(item.sort_order || 0)), ); const payload = { project: projectId, environment: activePage?.environment || 'dev', source_key: '', name: pageName.trim(), slug: slug.trim(), sort_order: maxSortOrder + 1, background_image_url: '', background_video_url: '', background_audio_url: '', background_loop: false, requires_auth: false, ui_schema_json: { elements: [] }, // Copy project design dimensions to new page design_width: project?.design_width ?? null, design_height: project?.design_height ?? null, }; try { setIsCreatingPage(true); const response = await axios.post('/tour_pages', { data: payload }); const createdPage = response?.data; await onReload(); if (createdPage?.id) { onSetActivePageId(createdPage.id); } onSetMenuOpen?.(true); onSuccess?.('New page created. You can now configure it in constructor.'); } 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 create page.'; logger.error( 'Failed to create page from constructor:', error instanceof Error ? error : { error }, ); onError?.(message); } finally { setIsCreatingPage(false); } }, [ activePage?.environment, onError, onReload, onSetActivePageId, onSetMenuOpen, onSuccess, pages, project?.design_width, project?.design_height, projectId, ]); const createTransition = useCallback( async (params: { name?: string; videoUrl: string; supportsReverse?: boolean; durationSec?: number; }) => { if (!projectId) { onError?.('Project is required.'); return; } const sanitizedVideoUrl = String(params.videoUrl || '').trim(); if (!sanitizedVideoUrl) { onError?.('Select a transition video asset first.'); return; } if (!params.durationSec) { onError?.( 'Could not resolve transition video duration yet. Please wait a moment and try again.', ); return; } try { setIsCreatingTransition(true); // Transitions are now stored directly in navigation elements as transitionVideoUrl // This method is kept for backwards compatibility but just shows a message onSuccess?.( 'Transition video can be set directly on navigation elements.', ); } 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 create transition.'; logger.error( 'Failed to create transition from constructor:', error instanceof Error ? error : { error }, ); onError?.(message); } finally { setIsCreatingTransition(false); } }, [projectId, onError, onSuccess], ); return { isSaving, isSavingToStage, isCreatingPage, isCreatingTransition, saveConstructor, saveToStage, createPage, createTransition, }; } export default useConstructorPageActions;