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'
|
||||
| 'create_transition';
|
||||
|
||||
type ConstructorPageProps = {
|
||||
mode?: 'constructor' | 'element_edit';
|
||||
};
|
||||
|
||||
const parseJsonObject = <T,>(value?: unknown, fallback?: T): T => {
|
||||
if (!value) return (fallback || ({} as T)) as T;
|
||||
|
||||
@ -524,17 +528,22 @@ const getElementButtonTitle = (element: CanvasElement) => {
|
||||
return element.label;
|
||||
};
|
||||
|
||||
const ConstructorPage = () => {
|
||||
const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
const router = useRouter();
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const elementEditorRef = useRef<HTMLDivElement>(null);
|
||||
const [isAuthReady, setIsAuthReady] = useState(false);
|
||||
const isElementEditMode = mode === 'element_edit';
|
||||
|
||||
const projectId = useMemo(() => {
|
||||
const value = router.query.projectId;
|
||||
if (Array.isArray(value)) return value[0] || '';
|
||||
return String(value || '');
|
||||
}, [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 value = router.query.pageId;
|
||||
@ -925,8 +934,12 @@ const ConstructorPage = () => {
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return;
|
||||
if (projectId) return;
|
||||
router.replace('/projects/projects-list');
|
||||
}, [projectId, router]);
|
||||
router.replace(
|
||||
isElementEditMode
|
||||
? '/page_elements/page_elements-list'
|
||||
: '/projects/projects-list',
|
||||
);
|
||||
}, [isElementEditMode, projectId, router]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@ -1939,7 +1952,9 @@ const ConstructorPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Constructor')}</title>
|
||||
<title>
|
||||
{getPageTitle(isElementEditMode ? 'Edit Element' : 'Constructor')}
|
||||
</title>
|
||||
</Head>
|
||||
<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'>
|
||||
@ -1957,7 +1972,7 @@ const ConstructorPage = () => {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{pages.length > 0 && (
|
||||
{pages.length > 0 && !isElementEditMode && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<select
|
||||
className='border border-gray-300 rounded px-3 py-2 bg-white text-sm'
|
||||
@ -1982,6 +1997,24 @@ const ConstructorPage = () => {
|
||||
/>
|
||||
</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
|
||||
@ -2928,7 +2961,7 @@ const ConstructorPage = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pages.length > 0 && (
|
||||
{pages.length > 0 && !isElementEditMode && (
|
||||
<div
|
||||
className='fixed z-40 w-60 border border-gray-200 rounded-lg bg-white shadow-xl'
|
||||
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 Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
@ -161,22 +161,8 @@ const PagesElementsListPage = () => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
const constructorHref = routeProjectId
|
||||
? `/constructor?projectId=${routeProjectId}`
|
||||
: '/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,
|
||||
},
|
||||
});
|
||||
};
|
||||
const getElementEditorHref = (item: ProjectElementItem) =>
|
||||
`/page_elements/page_elements-project-edit/?projectId=${encodeURIComponent(routeProjectId)}&pageId=${encodeURIComponent(item.pageId)}&elementId=${encodeURIComponent(item.id)}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -198,19 +184,6 @@ const PagesElementsListPage = () => {
|
||||
: projectName || 'No project selected'}
|
||||
</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 ? (
|
||||
<CardBox className='mb-6 text-sm text-red-600'>
|
||||
{errorMessage}
|
||||
@ -229,17 +202,16 @@ const PagesElementsListPage = () => {
|
||||
) : (
|
||||
<div className='space-y-2'>
|
||||
{projectElements.map((item) => (
|
||||
<button
|
||||
<Link
|
||||
key={`${item.pageId}_${item.id}`}
|
||||
type='button'
|
||||
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'
|
||||
onClick={() => openElementInEditor(item)}
|
||||
href={getElementEditorHref(item)}
|
||||
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'
|
||||
>
|
||||
<p className='text-sm font-semibold'>{item.name}</p>
|
||||
<p className='text-xs text-gray-500'>
|
||||
{item.pageName} • {toElementLabel(item.elementType)}
|
||||
</p>
|
||||
</button>
|
||||
</Link>
|
||||
))}
|
||||
</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