39948-vm/frontend/src/components/TourFlowManager.tsx
2026-05-28 07:19:36 +00:00

1042 lines
34 KiB
TypeScript

import {
mdiChartTimelineVariant,
mdiClose,
mdiFileDocumentPlus,
mdiSwapHorizontal,
mdiViewDashboard,
mdiChevronDown,
mdiChevronUp,
mdiPencil,
} from '@mdi/js';
import Icon from '@mdi/react';
import axios from 'axios';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import BaseButton from './BaseButton';
import CardBox from './CardBox';
import CardBoxModal from './CardBoxModal';
import SectionMain from './SectionMain';
import SectionTitleLineWithButton from './SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { hasPermission } from '../helpers/userPermissions';
import { useAppSelector, useAppDispatch } from '../stores/hooks';
import { logger } from '../lib/logger';
import { sanitizeSlug, buildUniqueSlug } from '../lib/slugHelpers';
import {
toRoutePath,
compareRoutes,
getProjectId,
getRows,
} from '../lib/tourFlowHelpers';
import { fetch as fetchGlobalTransitionDefaults } from '../stores/global_transition_defaults/globalTransitionDefaultsSlice';
import {
fetchByProjectAndEnv,
upsertByProjectAndEnv,
deleteByProjectAndEnv,
selectByProjectAndEnv,
selectIsLoading as selectTransitionSettingsLoading,
} from '../stores/project_transition_settings/projectTransitionSettingsSlice';
import type {
ProjectTransitionSettings,
TransitionType,
EasingFunction,
} from '../types/transition';
import { entityToProjectSettings } from '../types/transition';
type TourPage = {
id: string;
name?: string;
slug?: string;
sort_order?: number;
environment?: string;
projectId?: string;
project?: { id?: string } | string;
};
type Transition = {
id: string;
name?: string;
slug?: string;
environment?: string;
projectId?: string;
createdAt?: string;
project?: { id?: string } | string;
};
type ProjectOption = {
id: string;
label: string;
};
type ListEntry = {
type: 'page' | 'transition';
id: string;
title: string;
routeLabel: string;
description: string;
parentPageId: string;
};
const TRANSITION_TYPES: { value: TransitionType | ''; label: string }[] = [
{ value: '', label: 'Use Global Default' },
{ value: 'fade', label: 'Fade' },
{ value: 'none', label: 'None (instant)' },
];
const EASING_OPTIONS: { value: EasingFunction | ''; label: string }[] = [
{ value: '', label: 'Use Global Default' },
{ value: 'ease-in-out', label: 'Ease In-Out' },
{ value: 'ease-in', label: 'Ease In' },
{ value: 'ease-out', label: 'Ease Out' },
{ value: 'linear', label: 'Linear' },
];
const TourFlowManager = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth);
const globalDefaults = useAppSelector(
(state) => state.global_transition_defaults.data,
);
const routeProjectId = useMemo(() => {
const value = router.query.projectId;
if (Array.isArray(value)) return value[0] || '';
return String(value || '');
}, [router.query.projectId]);
const [pages, setPages] = useState<TourPage[]>([]);
const [transitions, setTransitions] = useState<Transition[]>([]);
const [projects, setProjects] = useState<ProjectOption[]>([]);
const [selectedProjectId, setSelectedProjectId] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [isCreatingPage, setIsCreatingPage] = useState(false);
const [isCreatingTransition, setIsCreatingTransition] = useState(false);
const [isCreatePageModalActive, setIsCreatePageModalActive] = useState(false);
const [newPageName, setNewPageName] = useState('');
const [deletingId, setDeletingId] = 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
const projectTransitionSettingsEntity = useAppSelector((state) =>
selectedProjectId
? selectByProjectAndEnv(state, selectedProjectId, 'dev')
: undefined,
);
const isTransitionSettingsLoading = useAppSelector((state) =>
selectedProjectId
? selectTransitionSettingsLoading(state, selectedProjectId, 'dev')
: false,
);
// Project transition settings state
const [isTransitionSettingsExpanded, setIsTransitionSettingsExpanded] =
useState(false);
// Convert entity to camelCase for local form state
const projectTransitionSettings = useMemo(
() => entityToProjectSettings(projectTransitionSettingsEntity),
[projectTransitionSettingsEntity],
);
const [localTransitionType, setLocalTransitionType] = useState<
TransitionType | ''
>('');
const [localDurationMs, setLocalDurationMs] = useState<number | ''>('');
const [localEasing, setLocalEasing] = useState<EasingFunction | ''>('');
const [localOverlayColor, setLocalOverlayColor] = useState<string>('');
const [isSavingTransitionSettings, setIsSavingTransitionSettings] =
useState(false);
const [transitionSaveSuccess, setTransitionSaveSuccess] = useState(false);
const canCreatePage = hasPermission(currentUser, 'CREATE_TOUR_PAGES');
const canCreateTransition = hasPermission(currentUser, 'CREATE_TRANSITIONS');
const canDeletePage = hasPermission(currentUser, 'DELETE_TOUR_PAGES');
const canDeleteTransition = hasPermission(currentUser, 'DELETE_TRANSITIONS');
const loadProjects = useCallback(async () => {
const response = await axios.get('/projects/autocomplete?limit=100');
const projectOptions = Array.isArray(response?.data) ? response.data : [];
setProjects(projectOptions);
if (projectOptions.length === 0) {
toast('Please create a project first', {
type: 'info',
position: 'bottom-center',
});
router.replace('/projects/projects-new');
return '';
}
if (
routeProjectId &&
projectOptions.some((item: ProjectOption) => item.id === routeProjectId)
) {
setSelectedProjectId(routeProjectId);
return routeProjectId;
}
if (projectOptions.length > 0) {
setSelectedProjectId((prev) => prev || projectOptions[0].id);
return projectOptions[0].id;
}
setSelectedProjectId('');
return '';
}, [routeProjectId, router]);
const loadData = useCallback(async () => {
try {
setIsLoading(true);
setErrorMessage('');
const projectId = selectedProjectId || (await loadProjects());
if (!projectId) {
setPages([]);
setTransitions([]);
setIsLoading(false);
return;
}
const pagesResponse = await axios.get(
`/tour_pages?limit=500&sort=asc&field=sort_order&project=${projectId}&environment=dev`,
);
setPages(getRows(pagesResponse));
setTransitions([]);
} catch (error: unknown) {
const axiosError = error as {
response?: { data?: { message?: string } };
};
setErrorMessage(
axiosError?.response?.data?.message ||
(error instanceof Error ? error.message : null) ||
'Failed to load pages and transitions.',
);
logger.error(
'Failed to load merged pages/transitions list:',
error instanceof Error ? error : { error },
);
} finally {
setIsLoading(false);
}
}, [loadProjects, selectedProjectId]);
useEffect(() => {
loadData();
}, [loadData]);
// Fetch global transition defaults
useEffect(() => {
dispatch(fetchGlobalTransitionDefaults());
}, [dispatch]);
// Load project transition settings when project changes
useEffect(() => {
if (!selectedProjectId) {
setLocalTransitionType('');
setLocalDurationMs('');
setLocalEasing('');
setLocalOverlayColor('');
return;
}
// Dispatch fetch for dev environment settings
dispatch(
fetchByProjectAndEnv({
projectId: selectedProjectId,
environment: 'dev',
}),
);
}, [selectedProjectId, dispatch]);
// Sync local form state when store data changes
useEffect(() => {
setLocalTransitionType(projectTransitionSettings?.transitionType ?? '');
setLocalDurationMs(projectTransitionSettings?.durationMs ?? '');
setLocalEasing(projectTransitionSettings?.easing ?? '');
setLocalOverlayColor(projectTransitionSettings?.overlayColor ?? '');
}, [projectTransitionSettings]);
useEffect(() => {
if (!selectedProjectId) return;
if (routeProjectId && selectedProjectId === routeProjectId) return;
if (routeProjectId) return;
router.replace(
{
pathname: '/tour_pages/tour_pages-list',
query: { projectId: selectedProjectId },
},
undefined,
{ shallow: true },
);
}, [routeProjectId, router, selectedProjectId]);
const sortedPages = useMemo(
() =>
[...pages].sort((first, second) => {
const routeCompare = compareRoutes(first.slug, second.slug);
if (routeCompare !== 0) return routeCompare;
const orderCompare = (first.sort_order || 0) - (second.sort_order || 0);
if (orderCompare !== 0) return orderCompare;
return (first.name || '').localeCompare(second.name || '');
}),
[pages],
);
const sortedTransitions = useMemo(
() =>
[...transitions].sort((first, second) => {
const dateFirst = first.createdAt
? new Date(first.createdAt).getTime()
: 0;
const dateSecond = second.createdAt
? new Date(second.createdAt).getTime()
: 0;
if (dateFirst !== dateSecond) return dateFirst - dateSecond;
return (first.name || first.slug || '').localeCompare(
second.name || second.slug || '',
);
}),
[transitions],
);
const activeProjectId = useMemo(() => {
if (selectedProjectId) return selectedProjectId;
const fromPages = sortedPages.find((item) => getProjectId(item));
if (fromPages) return getProjectId(fromPages);
const fromTransitions = sortedTransitions.find((item) =>
getProjectId(item),
);
if (fromTransitions) return getProjectId(fromTransitions);
return projects[0]?.id || '';
}, [projects, selectedProjectId, sortedPages, sortedTransitions]);
const targetEnvironment = useMemo(
() =>
sortedPages[0]?.environment || sortedTransitions[0]?.environment || 'dev',
[sortedPages, sortedTransitions],
);
const pageSlugsInEnvironment = useMemo(
() =>
new Set(
pages
.filter((item) => (item.environment || 'dev') === targetEnvironment)
.map((item) => String(item.slug || '').trim())
.filter(Boolean),
),
[pages, targetEnvironment],
);
const nameValidationError = useMemo(() => {
const name = newPageName.trim();
if (!name) return 'Page name is required.';
if (name.length > 255) return 'Page name must be 255 characters or less.';
return '';
}, [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 listEntries = useMemo<ListEntry[]>(() => {
const entries: ListEntry[] = [];
sortedPages.forEach((page, index) => {
const route = toRoutePath(page.slug);
entries.push({
type: 'page',
id: page.id,
title: page.name || `Page ${index + 1}`,
routeLabel: route,
description: 'Page',
parentPageId: page.id,
});
const transition = sortedTransitions[index];
if (!transition) return;
entries.push({
type: 'transition',
id: transition.id,
title: transition.name || transition.slug || `Transition ${index + 1}`,
routeLabel: route,
description: `Transition after ${route}`,
parentPageId: page.id,
});
});
return entries;
}, [sortedPages, sortedTransitions]);
const openConstructor = (
pageId: string,
sourceId: string,
sourceType: 'page' | 'transition',
) => {
if (!activeProjectId) {
router.push('/projects/projects-list');
return;
}
router.push({
pathname: '/constructor',
query: {
projectId: activeProjectId,
pageId,
sourceId,
sourceType,
},
});
};
const openCreatePageModal = () => {
if (!activeProjectId) {
setErrorMessage('Project not found. Please create a project first.');
return;
}
const suggestedName = `Page ${nextPageNumber}`;
setNewPageName(suggestedName);
setIsCreatePageModalActive(true);
};
const handleNameChange = (value: string) => {
setNewPageName(value);
};
const closeCreatePageModal = () => {
setIsCreatePageModalActive(false);
setNewPageName('');
};
// 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 () => {
if (!activeProjectId) {
setErrorMessage('Project not found. Please create a project first.');
return;
}
const name = newPageName.trim();
// Validate name
if (nameValidationError) {
return;
}
try {
setIsCreatingPage(true);
setErrorMessage('');
const payload = {
project: activeProjectId,
environment: targetEnvironment,
source_key: '',
name,
slug: generatedSlug,
sort_order:
Math.max(...pages.map((item) => Number(item.sort_order || 0)), 0) + 1,
background_image_url: '',
background_video_url: '',
background_audio_url: '',
background_loop: false,
requires_auth: false,
ui_schema_json: '',
};
await axios.post('/tour_pages', { data: payload });
closeCreatePageModal();
await loadData();
} 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.';
setErrorMessage(message);
logger.error(
'Failed to create page:',
error instanceof Error ? error : { error },
);
} finally {
setIsCreatingPage(false);
}
};
const handleCreateTransition = async () => {
// Transitions are now set directly on navigation elements as transitionVideoUrl
toast.info('Transitions are configured directly on navigation elements.');
};
const handleSaveTransitionSettings = async () => {
if (!selectedProjectId) return;
setIsSavingTransitionSettings(true);
setTransitionSaveSuccess(false);
try {
// Check if all values are empty (should delete to use global defaults)
const hasValues =
localTransitionType ||
localDurationMs !== '' ||
localEasing ||
localOverlayColor;
if (!hasValues) {
// Delete the settings record to revert to global defaults
await dispatch(
deleteByProjectAndEnv({
projectId: selectedProjectId,
environment: 'dev',
}),
).unwrap();
} else {
// Build the settings object with snake_case keys for the backend
const settingsToSave = {
transition_type: localTransitionType || 'fade',
duration_ms:
localDurationMs !== '' ? (localDurationMs as number) : 700,
easing: localEasing || 'ease-in-out',
overlay_color: localOverlayColor || '#000000',
};
await dispatch(
upsertByProjectAndEnv({
projectId: selectedProjectId,
environment: 'dev',
data: settingsToSave,
}),
).unwrap();
}
setTransitionSaveSuccess(true);
setTimeout(() => setTransitionSaveSuccess(false), 2000);
} catch (error) {
logger.error('Failed to save project transition settings:', error);
toast.error('Failed to save transition settings');
} finally {
setIsSavingTransitionSettings(false);
}
};
const handleDelete = async (
event: React.MouseEvent,
id: string,
type: 'page' | 'transition',
) => {
event.stopPropagation();
try {
setDeletingId(id);
setErrorMessage('');
if (type === 'page') {
await axios.delete(`/tour_pages/${id}`);
setPages((prev) => prev.filter((item) => item.id !== id));
}
} catch (error: unknown) {
const axiosError = error as {
response?: { data?: { message?: string } };
};
setErrorMessage(
axiosError?.response?.data?.message ||
(error instanceof Error ? error.message : null) ||
'Failed to delete item.',
);
logger.error(
'Failed to delete item:',
error instanceof Error ? error : { error },
);
} finally {
setDeletingId('');
}
};
return (
<>
<Head>
<title>{getPageTitle('Pages & Transitions')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Pages & Transitions'
main
>
{''}
</SectionTitleLineWithButton>
<p className='mb-6 text-sm font-semibold'>
{isLoading
? 'Loading project...'
: projects.find((project) => project.id === activeProjectId)
?.label || 'No project selected'}
</p>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap gap-3'>
<BaseButton
color='info'
icon={mdiFileDocumentPlus}
label='Create Page'
onClick={openCreatePageModal}
disabled={
!canCreatePage ||
isCreatingPage ||
isCreatingTransition ||
!activeProjectId
}
/>
<BaseButton
color='info'
icon={mdiSwapHorizontal}
label={
isCreatingTransition
? 'Creating Transition...'
: 'Create Transition'
}
onClick={handleCreateTransition}
disabled={
!canCreateTransition ||
isCreatingPage ||
isCreatingTransition ||
!activeProjectId
}
/>
<BaseButton
color='lightDark'
icon={mdiViewDashboard}
label='To Constructor'
href={
activeProjectId
? `/constructor?projectId=${activeProjectId}`
: '/projects/projects-list'
}
/>
</CardBox>
{/* Project Transition Settings */}
{selectedProjectId && (
<CardBox className='mb-6'>
<button
type='button'
className='flex w-full items-center justify-between text-left'
onClick={() =>
setIsTransitionSettingsExpanded(!isTransitionSettingsExpanded)
}
>
<h3 className='text-sm font-semibold text-gray-700 dark:text-gray-300'>
Project Transition Settings
</h3>
<Icon
path={
isTransitionSettingsExpanded ? mdiChevronUp : mdiChevronDown
}
size={0.8}
className='text-gray-500'
/>
</button>
{isTransitionSettingsExpanded && (
<div className='mt-4'>
<p className='mb-4 text-xs text-gray-500 dark:text-gray-400'>
Override global transition defaults for this project (dev
environment). Changes are copied to Stage when you &quot;Save
to Stage&quot; and to Production when you &quot;Publish&quot;.
Leave empty to use global defaults.
{globalDefaults && (
<span className='ml-1'>
(Global: {globalDefaults.transition_type},{' '}
{globalDefaults.duration_ms}ms, {globalDefaults.easing},{' '}
{globalDefaults.overlay_color ?? '#000000'})
</span>
)}
</p>
<div className='grid grid-cols-1 gap-4 md:grid-cols-4'>
<div>
<label className='mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400'>
Transition Type
</label>
<select
className='w-full rounded border border-gray-300 px-3 py-2 text-sm dark:border-dark-600 dark:bg-dark-800'
value={localTransitionType}
onChange={(e) =>
setLocalTransitionType(
e.target.value as TransitionType | '',
)
}
>
{TRANSITION_TYPES.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div>
<label className='mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400'>
Duration (ms)
</label>
<input
type='number'
min='0'
placeholder='Use global default'
className='w-full rounded border border-gray-300 px-3 py-2 text-sm dark:border-dark-600 dark:bg-dark-800'
value={localDurationMs}
onChange={(e) => {
const val = e.target.value;
setLocalDurationMs(
val === '' ? '' : Math.max(0, parseInt(val, 10) || 0),
);
}}
/>
</div>
<div>
<label className='mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400'>
Easing
</label>
<select
className='w-full rounded border border-gray-300 px-3 py-2 text-sm dark:border-dark-600 dark:bg-dark-800'
value={localEasing}
onChange={(e) =>
setLocalEasing(e.target.value as EasingFunction | '')
}
>
{EASING_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div>
<label className='mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400'>
Overlay Color
</label>
<div className='flex gap-2'>
<input
type='color'
className='h-9 w-12 cursor-pointer rounded border border-gray-300 p-0.5 dark:border-dark-600'
value={localOverlayColor || '#000000'}
onChange={(e) => setLocalOverlayColor(e.target.value)}
/>
<input
type='text'
placeholder='Use global'
className='flex-1 rounded border border-gray-300 px-3 py-2 text-sm dark:border-dark-600 dark:bg-dark-800'
value={localOverlayColor}
onChange={(e) => setLocalOverlayColor(e.target.value)}
/>
</div>
</div>
</div>
<div className='mt-4 flex items-center gap-3'>
<BaseButton
label={
isSavingTransitionSettings ? 'Saving...' : 'Save Settings'
}
color='info'
small
onClick={handleSaveTransitionSettings}
disabled={isSavingTransitionSettings}
/>
{transitionSaveSuccess && (
<span className='text-xs text-green-600'>
Saved successfully!
</span>
)}
</div>
</div>
)}
</CardBox>
)}
<CardBoxModal
title='Create page'
buttonColor='info'
buttonLabel={isCreatingPage ? 'Creating...' : 'Create'}
isConfirmDisabled={Boolean(nameValidationError) || isCreatingPage}
isActive={isCreatePageModalActive}
onConfirm={handleCreatePage}
onCancel={isCreatingPage ? undefined : closeCreatePageModal}
>
<div>
<label
htmlFor='new-page-name'
className='block text-sm font-semibold mb-1'
>
Page name
</label>
<input
id='new-page-name'
type='text'
value={newPageName}
onChange={(event) => handleNameChange(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}
/>
{nameValidationError && (
<p className='text-xs text-red-600 mt-1'>{nameValidationError}</p>
)}
</div>
</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>
)}
</div>
</CardBoxModal>
{errorMessage && (
<CardBox className='mb-6'>
<p className='text-sm text-red-600'>{errorMessage}</p>
</CardBox>
)}
<CardBox>
{isLoading ? (
<p className='text-sm text-gray-500'>
Loading pages and transitions...
</p>
) : listEntries.length === 0 ? (
<p className='text-sm text-gray-500'>
No pages or transitions yet.
</p>
) : (
<ul className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
{listEntries.map((entry) => {
const canDelete =
entry.type === 'page' ? canDeletePage : canDeleteTransition;
const isDeleting = deletingId === entry.id;
const pageData =
entry.type === 'page'
? pages.find((p) => p.id === entry.id)
: null;
return (
<li key={`${entry.type}-${entry.id}`}>
<div
role='button'
tabIndex={0}
className='w-full text-left border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors relative cursor-pointer'
onClick={() =>
openConstructor(
entry.parentPageId,
entry.id,
entry.type,
)
}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openConstructor(
entry.parentPageId,
entry.id,
entry.type,
);
}
}}
>
<div className='pr-20'>
<p className='text-xs uppercase text-gray-500 mb-1'>
{entry.description}
</p>
<p className='font-semibold'>{entry.title}</p>
<p className='text-sm text-gray-500 mt-1'>
Route: {entry.routeLabel}
</p>
</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
icon={mdiClose}
color='danger'
outline
small
onClick={(event) =>
handleDelete(event, entry.id, entry.type)
}
disabled={!canDelete || isDeleting}
/>
</div>
</div>
</li>
);
})}
</ul>
)}
</CardBox>
</SectionMain>
</>
);
};
export default TourFlowManager;