Autosave: 20260319-064134
This commit is contained in:
parent
013560f0c1
commit
d21a76e602
BIN
assets/pasted-20260319-061300-6728d60c.jpg
Normal file
BIN
assets/pasted-20260319-061300-6728d60c.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/pasted-20260319-061442-0a583e0c.png
Normal file
BIN
assets/pasted-20260319-061442-0a583e0c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
BIN
frontend/public/assets/vm-shot-2026-03-19T06-13-19-234Z.jpg
Normal file
BIN
frontend/public/assets/vm-shot-2026-03-19T06-13-19-234Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
frontend/public/assets/vm-shot-2026-03-19T06-13-54-729Z.jpg
Normal file
BIN
frontend/public/assets/vm-shot-2026-03-19T06-13-54-729Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@ -146,6 +146,10 @@ type EditorMenuItem =
|
|||||||
| 'background_audio'
|
| 'background_audio'
|
||||||
| 'create_transition';
|
| 'create_transition';
|
||||||
|
|
||||||
|
type ConstructorPageProps = {
|
||||||
|
mode?: 'constructor' | 'element_edit';
|
||||||
|
};
|
||||||
|
|
||||||
const parseJsonObject = <T,>(value?: unknown, fallback?: T): T => {
|
const parseJsonObject = <T,>(value?: unknown, fallback?: T): T => {
|
||||||
if (!value) return (fallback || ({} as T)) as T;
|
if (!value) return (fallback || ({} as T)) as T;
|
||||||
|
|
||||||
@ -524,17 +528,22 @@ const getElementButtonTitle = (element: CanvasElement) => {
|
|||||||
return element.label;
|
return element.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConstructorPage = () => {
|
const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
const elementEditorRef = useRef<HTMLDivElement>(null);
|
const elementEditorRef = useRef<HTMLDivElement>(null);
|
||||||
const [isAuthReady, setIsAuthReady] = useState(false);
|
const [isAuthReady, setIsAuthReady] = useState(false);
|
||||||
|
const isElementEditMode = mode === 'element_edit';
|
||||||
|
|
||||||
const projectId = useMemo(() => {
|
const projectId = useMemo(() => {
|
||||||
const value = router.query.projectId;
|
const value = router.query.projectId;
|
||||||
if (Array.isArray(value)) return value[0] || '';
|
if (Array.isArray(value)) return value[0] || '';
|
||||||
return String(value || '');
|
return String(value || '');
|
||||||
}, [router.query.projectId]);
|
}, [router.query.projectId]);
|
||||||
|
const pageElementsListHref = useMemo(() => {
|
||||||
|
if (!projectId) return '/page_elements/page_elements-list';
|
||||||
|
return `/page_elements/page_elements-list?projectId=${encodeURIComponent(projectId)}`;
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
const pageIdFromRoute = useMemo(() => {
|
const pageIdFromRoute = useMemo(() => {
|
||||||
const value = router.query.pageId;
|
const value = router.query.pageId;
|
||||||
@ -925,8 +934,12 @@ const ConstructorPage = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.isReady) return;
|
if (!router.isReady) return;
|
||||||
if (projectId) return;
|
if (projectId) return;
|
||||||
router.replace('/projects/projects-list');
|
router.replace(
|
||||||
}, [projectId, router]);
|
isElementEditMode
|
||||||
|
? '/page_elements/page_elements-list'
|
||||||
|
: '/projects/projects-list',
|
||||||
|
);
|
||||||
|
}, [isElementEditMode, projectId, router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
@ -1939,7 +1952,9 @@ const ConstructorPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Constructor')}</title>
|
<title>
|
||||||
|
{getPageTitle(isElementEditMode ? 'Edit Element' : 'Constructor')}
|
||||||
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className='relative w-screen h-screen bg-white overflow-hidden'>
|
<div className='relative w-screen h-screen bg-white overflow-hidden'>
|
||||||
<div className='absolute top-4 left-4 z-30 flex max-w-[80vw] flex-col gap-2'>
|
<div className='absolute top-4 left-4 z-30 flex max-w-[80vw] flex-col gap-2'>
|
||||||
@ -1957,7 +1972,7 @@ const ConstructorPage = () => {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{pages.length > 0 && (
|
{pages.length > 0 && !isElementEditMode && (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<select
|
<select
|
||||||
className='border border-gray-300 rounded px-3 py-2 bg-white text-sm'
|
className='border border-gray-300 rounded px-3 py-2 bg-white text-sm'
|
||||||
@ -1982,6 +1997,24 @@ const ConstructorPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{pages.length > 0 && isElementEditMode && (
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<BaseButton
|
||||||
|
color='lightDark'
|
||||||
|
label='Back to Elements'
|
||||||
|
icon={mdiExitToApp}
|
||||||
|
href={pageElementsListHref}
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
color='info'
|
||||||
|
label={isSaving ? 'Saving...' : 'Save'}
|
||||||
|
icon={mdiContentSave}
|
||||||
|
onClick={saveConstructor}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -2928,7 +2961,7 @@ const ConstructorPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{pages.length > 0 && (
|
{pages.length > 0 && !isElementEditMode && (
|
||||||
<div
|
<div
|
||||||
className='fixed z-40 w-60 border border-gray-200 rounded-lg bg-white shadow-xl'
|
className='fixed z-40 w-60 border border-gray-200 rounded-lg bg-white shadow-xl'
|
||||||
style={{ left: menuPosition.x, top: menuPosition.y }}
|
style={{ left: menuPosition.x, top: menuPosition.y }}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { mdiChartTimelineVariant, mdiViewDashboard } from '@mdi/js';
|
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import BaseButton from '../../components/BaseButton';
|
|
||||||
import CardBox from '../../components/CardBox';
|
import CardBox from '../../components/CardBox';
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||||
import SectionMain from '../../components/SectionMain';
|
import SectionMain from '../../components/SectionMain';
|
||||||
@ -161,22 +161,8 @@ const PagesElementsListPage = () => {
|
|||||||
loadData();
|
loadData();
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
const constructorHref = routeProjectId
|
const getElementEditorHref = (item: ProjectElementItem) =>
|
||||||
? `/constructor?projectId=${routeProjectId}`
|
`/page_elements/page_elements-project-edit/?projectId=${encodeURIComponent(routeProjectId)}&pageId=${encodeURIComponent(item.pageId)}&elementId=${encodeURIComponent(item.id)}`;
|
||||||
: '/constructor';
|
|
||||||
|
|
||||||
const openElementInEditor = (item: ProjectElementItem) => {
|
|
||||||
if (!routeProjectId) return;
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: '/page_elements/page_elements-project-edit',
|
|
||||||
query: {
|
|
||||||
projectId: routeProjectId,
|
|
||||||
pageId: item.pageId,
|
|
||||||
elementId: item.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -198,19 +184,6 @@ const PagesElementsListPage = () => {
|
|||||||
: projectName || 'No project selected'}
|
: projectName || 'No project selected'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CardBox
|
|
||||||
className='mb-6'
|
|
||||||
cardBoxClassName='flex flex-wrap items-start gap-3'
|
|
||||||
>
|
|
||||||
<BaseButton
|
|
||||||
color='lightDark'
|
|
||||||
icon={mdiViewDashboard}
|
|
||||||
href={constructorHref}
|
|
||||||
label='To Constructor'
|
|
||||||
disabled={!routeProjectId}
|
|
||||||
/>
|
|
||||||
</CardBox>
|
|
||||||
|
|
||||||
{errorMessage ? (
|
{errorMessage ? (
|
||||||
<CardBox className='mb-6 text-sm text-red-600'>
|
<CardBox className='mb-6 text-sm text-red-600'>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
@ -229,17 +202,16 @@ const PagesElementsListPage = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
{projectElements.map((item) => (
|
{projectElements.map((item) => (
|
||||||
<button
|
<Link
|
||||||
key={`${item.pageId}_${item.id}`}
|
key={`${item.pageId}_${item.id}`}
|
||||||
type='button'
|
href={getElementEditorHref(item)}
|
||||||
className='w-full rounded border border-gray-200 px-3 py-2 text-left hover:bg-gray-50 dark:border-dark-700 dark:hover:bg-dark-800'
|
className='block w-full rounded border border-gray-200 px-3 py-2 text-left hover:bg-gray-50 dark:border-dark-700 dark:hover:bg-dark-800'
|
||||||
onClick={() => openElementInEditor(item)}
|
|
||||||
>
|
>
|
||||||
<p className='text-sm font-semibold'>{item.name}</p>
|
<p className='text-sm font-semibold'>{item.name}</p>
|
||||||
<p className='text-xs text-gray-500'>
|
<p className='text-xs text-gray-500'>
|
||||||
{item.pageName} • {toElementLabel(item.elementType)}
|
{item.pageName} • {toElementLabel(item.elementType)}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,3 +1,540 @@
|
|||||||
import ConstructorPage from '../constructor';
|
import { mdiArrowLeft, mdiContentSave, mdiPencil } from '@mdi/js';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import BaseButton from '../../components/BaseButton';
|
||||||
|
import CardBox from '../../components/CardBox';
|
||||||
|
import SectionMain from '../../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||||
|
import { getPageTitle } from '../../config';
|
||||||
|
import { hasPermission } from '../../helpers/userPermissions';
|
||||||
|
import { useAppSelector } from '../../stores/hooks';
|
||||||
|
|
||||||
export default ConstructorPage;
|
type TourPage = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
sort_order?: number;
|
||||||
|
environment?: string;
|
||||||
|
source_key?: string;
|
||||||
|
requires_auth?: boolean;
|
||||||
|
ui_schema_json?: Record<string, any> | string;
|
||||||
|
background_image_url?: string;
|
||||||
|
background_video_url?: string;
|
||||||
|
background_audio_url?: string;
|
||||||
|
background_loop?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConstructorElement = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
label?: string;
|
||||||
|
xPercent?: number;
|
||||||
|
yPercent?: number;
|
||||||
|
appearDelaySec?: number;
|
||||||
|
appearDurationSec?: number | null;
|
||||||
|
navLabel?: string;
|
||||||
|
targetPageId?: string;
|
||||||
|
tooltipTitle?: string;
|
||||||
|
tooltipText?: string;
|
||||||
|
descriptionTitle?: string;
|
||||||
|
descriptionText?: string;
|
||||||
|
mediaUrl?: string;
|
||||||
|
mediaAutoplay?: boolean;
|
||||||
|
mediaLoop?: boolean;
|
||||||
|
mediaMuted?: boolean;
|
||||||
|
galleryCards?: any[];
|
||||||
|
carouselSlides?: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConstructorSchema = {
|
||||||
|
elements?: ConstructorElement[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseJsonObject = <T,>(value?: unknown, fallback?: T): T => {
|
||||||
|
if (!value) return (fallback || ({} as T)) as T;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return (parsed || fallback || {}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (fallback || ({} as T)) as T;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse JSON on element edit page:', error);
|
||||||
|
return (fallback || ({} as T)) as T;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toStringQuery = (value: string | string[] | undefined) => {
|
||||||
|
if (Array.isArray(value)) return String(value[0] || '').trim();
|
||||||
|
return String(value || '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clampPercent = (value: string) => {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed)) return 0;
|
||||||
|
return Math.min(Math.max(parsed, 0), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseNullableNumber = (value: string) => {
|
||||||
|
if (!value.trim()) return null;
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseArrayJson = (value: string, fieldName: string) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
throw new Error(`${fieldName} must be a JSON array.`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to parse ${fieldName}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageElementsProjectEditPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_PAGE_ELEMENTS');
|
||||||
|
|
||||||
|
const projectId = useMemo(() => toStringQuery(router.query.projectId), [router.query.projectId]);
|
||||||
|
const pageId = useMemo(() => toStringQuery(router.query.pageId), [router.query.pageId]);
|
||||||
|
const elementId = useMemo(() => toStringQuery(router.query.elementId), [router.query.elementId]);
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState<TourPage | null>(null);
|
||||||
|
const [element, setElement] = useState<ConstructorElement | null>(null);
|
||||||
|
const [projectPages, setProjectPages] = useState<TourPage[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const [successMessage, setSuccessMessage] = useState('');
|
||||||
|
|
||||||
|
const [label, setLabel] = useState('');
|
||||||
|
const [xPercent, setXPercent] = useState('0');
|
||||||
|
const [yPercent, setYPercent] = useState('0');
|
||||||
|
const [appearDelaySec, setAppearDelaySec] = useState('0');
|
||||||
|
const [appearDurationSec, setAppearDurationSec] = useState('');
|
||||||
|
const [navLabel, setNavLabel] = useState('');
|
||||||
|
const [targetPageId, setTargetPageId] = useState('');
|
||||||
|
const [tooltipTitle, setTooltipTitle] = useState('');
|
||||||
|
const [tooltipText, setTooltipText] = useState('');
|
||||||
|
const [descriptionTitle, setDescriptionTitle] = useState('');
|
||||||
|
const [descriptionText, setDescriptionText] = useState('');
|
||||||
|
const [mediaUrl, setMediaUrl] = useState('');
|
||||||
|
const [mediaAutoplay, setMediaAutoplay] = useState(false);
|
||||||
|
const [mediaLoop, setMediaLoop] = useState(false);
|
||||||
|
const [mediaMuted, setMediaMuted] = useState(false);
|
||||||
|
const [galleryCardsJson, setGalleryCardsJson] = useState('[]');
|
||||||
|
const [carouselSlidesJson, setCarouselSlidesJson] = useState('[]');
|
||||||
|
|
||||||
|
const applyElementToForm = useCallback((item: ConstructorElement) => {
|
||||||
|
setLabel(String(item.label || ''));
|
||||||
|
setXPercent(String(item.xPercent ?? 0));
|
||||||
|
setYPercent(String(item.yPercent ?? 0));
|
||||||
|
setAppearDelaySec(String(item.appearDelaySec ?? 0));
|
||||||
|
setAppearDurationSec(item.appearDurationSec === null || item.appearDurationSec === undefined ? '' : String(item.appearDurationSec));
|
||||||
|
setNavLabel(String(item.navLabel || ''));
|
||||||
|
setTargetPageId(String(item.targetPageId || ''));
|
||||||
|
setTooltipTitle(String(item.tooltipTitle || ''));
|
||||||
|
setTooltipText(String(item.tooltipText || ''));
|
||||||
|
setDescriptionTitle(String(item.descriptionTitle || ''));
|
||||||
|
setDescriptionText(String(item.descriptionText || ''));
|
||||||
|
setMediaUrl(String(item.mediaUrl || ''));
|
||||||
|
setMediaAutoplay(Boolean(item.mediaAutoplay));
|
||||||
|
setMediaLoop(Boolean(item.mediaLoop));
|
||||||
|
setMediaMuted(Boolean(item.mediaMuted));
|
||||||
|
setGalleryCardsJson(JSON.stringify(item.galleryCards || [], null, 2));
|
||||||
|
setCarouselSlidesJson(JSON.stringify(item.carouselSlides || [], null, 2));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
if (!pageId || !elementId) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorMessage('');
|
||||||
|
setSuccessMessage('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pageResponse = await axios.get(`/tour_pages/${pageId}`);
|
||||||
|
const loadedPage = pageResponse?.data as TourPage;
|
||||||
|
|
||||||
|
const schema = parseJsonObject<ConstructorSchema>(loadedPage?.ui_schema_json, {});
|
||||||
|
const elements = Array.isArray(schema.elements) ? schema.elements : [];
|
||||||
|
const selectedElement = elements.find((item) => String(item?.id || '') === elementId);
|
||||||
|
|
||||||
|
if (!selectedElement) {
|
||||||
|
throw new Error('Element not found on this page.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPage(loadedPage);
|
||||||
|
setElement(selectedElement);
|
||||||
|
applyElementToForm(selectedElement);
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
const pagesResponse = await axios.get(
|
||||||
|
`/tour_pages?limit=500&page=0&sort=asc&field=sort_order&project=${encodeURIComponent(projectId)}`,
|
||||||
|
);
|
||||||
|
const rows = Array.isArray(pagesResponse?.data?.rows) ? pagesResponse.data.rows : [];
|
||||||
|
setProjectPages(rows);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const message =
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error?.message ||
|
||||||
|
'Failed to load element settings.';
|
||||||
|
console.error('Failed to load dedicated element edit page:', error);
|
||||||
|
setErrorMessage(message);
|
||||||
|
setCurrentPage(null);
|
||||||
|
setElement(null);
|
||||||
|
setProjectPages([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [applyElementToForm, elementId, pageId, projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
const saveElement = async () => {
|
||||||
|
if (!currentPage || !element || !hasUpdatePermission) return;
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
setErrorMessage('');
|
||||||
|
setSuccessMessage('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nextElement: ConstructorElement = {
|
||||||
|
...element,
|
||||||
|
label: label.trim(),
|
||||||
|
xPercent: clampPercent(xPercent),
|
||||||
|
yPercent: clampPercent(yPercent),
|
||||||
|
appearDelaySec: Number(appearDelaySec) >= 0 ? Number(appearDelaySec) : 0,
|
||||||
|
appearDurationSec: parseNullableNumber(appearDurationSec),
|
||||||
|
navLabel: navLabel.trim(),
|
||||||
|
targetPageId: targetPageId.trim(),
|
||||||
|
tooltipTitle: tooltipTitle.trim(),
|
||||||
|
tooltipText: tooltipText,
|
||||||
|
descriptionTitle: descriptionTitle.trim(),
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
mediaUrl: mediaUrl.trim(),
|
||||||
|
mediaAutoplay,
|
||||||
|
mediaLoop,
|
||||||
|
mediaMuted,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (element.type === 'gallery') {
|
||||||
|
nextElement.galleryCards = parseArrayJson(galleryCardsJson, 'Gallery cards');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.type === 'carousel') {
|
||||||
|
nextElement.carouselSlides = parseArrayJson(carouselSlidesJson, 'Carousel slides');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingSchema = parseJsonObject<ConstructorSchema>(currentPage.ui_schema_json, {});
|
||||||
|
const existingElements = Array.isArray(existingSchema.elements) ? existingSchema.elements : [];
|
||||||
|
const updatedElements = existingElements.map((item) =>
|
||||||
|
String(item.id) === String(element.id) ? nextElement : item,
|
||||||
|
);
|
||||||
|
|
||||||
|
await axios.put(`/tour_pages/${currentPage.id}`, {
|
||||||
|
id: currentPage.id,
|
||||||
|
data: {
|
||||||
|
environment: currentPage.environment,
|
||||||
|
source_key: currentPage.source_key,
|
||||||
|
name: currentPage.name,
|
||||||
|
slug: currentPage.slug,
|
||||||
|
sort_order: currentPage.sort_order,
|
||||||
|
requires_auth: currentPage.requires_auth,
|
||||||
|
ui_schema_json: {
|
||||||
|
...existingSchema,
|
||||||
|
elements: updatedElements,
|
||||||
|
},
|
||||||
|
background_image_url: currentPage.background_image_url,
|
||||||
|
background_video_url: currentPage.background_video_url,
|
||||||
|
background_audio_url: currentPage.background_audio_url,
|
||||||
|
background_loop: currentPage.background_loop,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setElement(nextElement);
|
||||||
|
setCurrentPage((previous) =>
|
||||||
|
previous
|
||||||
|
? {
|
||||||
|
...previous,
|
||||||
|
ui_schema_json: {
|
||||||
|
...existingSchema,
|
||||||
|
elements: updatedElements,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: previous,
|
||||||
|
);
|
||||||
|
setSuccessMessage('Element settings saved.');
|
||||||
|
} catch (error: any) {
|
||||||
|
const message =
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error?.message ||
|
||||||
|
'Failed to save element settings.';
|
||||||
|
console.error('Failed to save dedicated element settings:', error);
|
||||||
|
setErrorMessage(message);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const backHref = projectId
|
||||||
|
? `/page_elements/page_elements-list?projectId=${encodeURIComponent(projectId)}`
|
||||||
|
: '/page_elements/page_elements-list';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Edit Element')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiPencil} title='Edit Element Settings' main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<CardBox className='mb-4'>
|
||||||
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
|
<Link href={backHref}>
|
||||||
|
<BaseButton icon={mdiArrowLeft} label='Back to Elements' color='lightDark' />
|
||||||
|
</Link>
|
||||||
|
<BaseButton
|
||||||
|
color='success'
|
||||||
|
icon={mdiContentSave}
|
||||||
|
label={isSaving ? 'Saving...' : 'Save'}
|
||||||
|
onClick={saveElement}
|
||||||
|
disabled={isSaving || isLoading || !element || !hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{errorMessage ? <CardBox className='mb-4 text-sm text-red-600'>{errorMessage}</CardBox> : null}
|
||||||
|
{successMessage ? <CardBox className='mb-4 text-sm text-green-600'>{successMessage}</CardBox> : null}
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<CardBox>
|
||||||
|
<p className='text-sm text-gray-500'>Loading element settings...</p>
|
||||||
|
</CardBox>
|
||||||
|
) : !element ? (
|
||||||
|
<CardBox>
|
||||||
|
<p className='text-sm text-gray-500'>Element was not found.</p>
|
||||||
|
</CardBox>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CardBox className='mb-4'>
|
||||||
|
<p className='text-xs text-gray-500'>Element ID: {element.id}</p>
|
||||||
|
<p className='text-xs text-gray-500'>Page: {currentPage?.name || currentPage?.id}</p>
|
||||||
|
<p className='mt-1 text-sm font-semibold'>Type: {element.type}</p>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className='mb-4'>
|
||||||
|
<h3 className='mb-3 text-sm font-semibold'>General</h3>
|
||||||
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Label</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={label}
|
||||||
|
onChange={(event) => setLabel(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Target page</label>
|
||||||
|
<select
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={targetPageId}
|
||||||
|
onChange={(event) => setTargetPageId(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
>
|
||||||
|
<option value=''>None</option>
|
||||||
|
{projectPages.map((page) => (
|
||||||
|
<option key={page.id} value={page.id}>
|
||||||
|
{page.name || page.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>X percent</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={xPercent}
|
||||||
|
onChange={(event) => setXPercent(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Y percent</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={yPercent}
|
||||||
|
onChange={(event) => setYPercent(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Appear delay (sec)</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={appearDelaySec}
|
||||||
|
onChange={(event) => setAppearDelaySec(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Appear duration (sec)</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={appearDurationSec}
|
||||||
|
onChange={(event) => setAppearDurationSec(event.target.value)}
|
||||||
|
placeholder='Leave empty for none'
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className='mb-4'>
|
||||||
|
<h3 className='mb-3 text-sm font-semibold'>Navigation / Text Settings</h3>
|
||||||
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Navigation label</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={navLabel}
|
||||||
|
onChange={(event) => setNavLabel(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Tooltip title</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={tooltipTitle}
|
||||||
|
onChange={(event) => setTooltipTitle(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='md:col-span-2'>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Tooltip text</label>
|
||||||
|
<textarea
|
||||||
|
className='h-24 w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={tooltipText}
|
||||||
|
onChange={(event) => setTooltipText(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Description title</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={descriptionTitle}
|
||||||
|
onChange={(event) => setDescriptionTitle(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='md:col-span-2'>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Description text</label>
|
||||||
|
<textarea
|
||||||
|
className='h-24 w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={descriptionText}
|
||||||
|
onChange={(event) => setDescriptionText(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className='mb-4'>
|
||||||
|
<h3 className='mb-3 text-sm font-semibold'>Media</h3>
|
||||||
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
|
<div className='md:col-span-2'>
|
||||||
|
<label className='mb-1 block text-sm font-semibold'>Media URL</label>
|
||||||
|
<input
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-2 dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={mediaUrl}
|
||||||
|
onChange={(event) => setMediaUrl(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label className='flex items-center gap-2 text-sm'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={mediaAutoplay}
|
||||||
|
onChange={(event) => setMediaAutoplay(event.target.checked)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
Autoplay
|
||||||
|
</label>
|
||||||
|
<label className='flex items-center gap-2 text-sm'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={mediaLoop}
|
||||||
|
onChange={(event) => setMediaLoop(event.target.checked)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
Loop
|
||||||
|
</label>
|
||||||
|
<label className='flex items-center gap-2 text-sm'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={mediaMuted}
|
||||||
|
onChange={(event) => setMediaMuted(event.target.checked)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
Muted
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{element.type === 'gallery' ? (
|
||||||
|
<CardBox className='mb-4'>
|
||||||
|
<h3 className='mb-3 text-sm font-semibold'>Gallery cards JSON</h3>
|
||||||
|
<textarea
|
||||||
|
className='h-56 w-full rounded border border-gray-300 px-2 py-2 font-mono text-xs dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={galleryCardsJson}
|
||||||
|
onChange={(event) => setGalleryCardsJson(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</CardBox>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{element.type === 'carousel' ? (
|
||||||
|
<CardBox className='mb-4'>
|
||||||
|
<h3 className='mb-3 text-sm font-semibold'>Carousel slides JSON</h3>
|
||||||
|
<textarea
|
||||||
|
className='h-56 w-full rounded border border-gray-300 px-2 py-2 font-mono text-xs dark:border-dark-700 dark:bg-dark-800'
|
||||||
|
value={carouselSlidesJson}
|
||||||
|
onChange={(event) => setCarouselSlidesJson(event.target.value)}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</CardBox>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PageElementsProjectEditPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageElementsProjectEditPage;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user