Autosave: 20260319-061301
This commit is contained in:
parent
08ac54f0b5
commit
013560f0c1
BIN
frontend/public/assets/vm-shot-2026-03-19T06-12-36-229Z.jpg
Normal file
BIN
frontend/public/assets/vm-shot-2026-03-19T06-12-36-229Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@ -510,16 +510,10 @@ const getElementButtonTitle = (element: CanvasElement) => {
|
|||||||
return `${element.label} (${element.carouselSlides?.length || 0})`;
|
return `${element.label} (${element.carouselSlides?.length || 0})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.type === 'tooltip' && element.tooltipTitle)
|
if (element.type === 'tooltip') return element.tooltipTitle ?? '';
|
||||||
return element.tooltipTitle;
|
if (element.type === 'description') return element.descriptionTitle ?? '';
|
||||||
if (element.type === 'description' && element.descriptionTitle)
|
if (element.type === 'navigation_next' || element.type === 'navigation_prev')
|
||||||
return element.descriptionTitle;
|
return element.navLabel ?? '';
|
||||||
if (
|
|
||||||
(element.type === 'navigation_next' ||
|
|
||||||
element.type === 'navigation_prev') &&
|
|
||||||
element.navLabel
|
|
||||||
)
|
|
||||||
return element.navLabel;
|
|
||||||
if (
|
if (
|
||||||
(element.type === 'video_player' || element.type === 'audio_player') &&
|
(element.type === 'video_player' || element.type === 'audio_player') &&
|
||||||
element.mediaUrl
|
element.mediaUrl
|
||||||
@ -547,6 +541,11 @@ const ConstructorPage = () => {
|
|||||||
if (Array.isArray(value)) return value[0] || '';
|
if (Array.isArray(value)) return value[0] || '';
|
||||||
return String(value || '');
|
return String(value || '');
|
||||||
}, [router.query.pageId]);
|
}, [router.query.pageId]);
|
||||||
|
const elementIdFromRoute = useMemo(() => {
|
||||||
|
const value = router.query.elementId;
|
||||||
|
if (Array.isArray(value)) return value[0] || '';
|
||||||
|
return String(value || '');
|
||||||
|
}, [router.query.elementId]);
|
||||||
|
|
||||||
const [pages, setPages] = useState<TourPage[]>([]);
|
const [pages, setPages] = useState<TourPage[]>([]);
|
||||||
const [assets, setAssets] = useState<ProjectAsset[]>([]);
|
const [assets, setAssets] = useState<ProjectAsset[]>([]);
|
||||||
@ -577,6 +576,7 @@ const ConstructorPage = () => {
|
|||||||
const [resolvedDurationBySource, setResolvedDurationBySource] = useState<
|
const [resolvedDurationBySource, setResolvedDurationBySource] = useState<
|
||||||
Record<string, number | null>
|
Record<string, number | null>
|
||||||
>({});
|
>({});
|
||||||
|
const [canvasElapsedSec, setCanvasElapsedSec] = useState(0);
|
||||||
|
|
||||||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 110 });
|
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 110 });
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
@ -596,22 +596,15 @@ const ConstructorPage = () => {
|
|||||||
const reverseAnimationFrame = useRef<number | null>(null);
|
const reverseAnimationFrame = useRef<number | null>(null);
|
||||||
const didSetInitialCanvasFocus = useRef(false);
|
const didSetInitialCanvasFocus = useRef(false);
|
||||||
const durationProbeInFlightRef = useRef<Set<string>>(new Set());
|
const durationProbeInFlightRef = useRef<Set<string>>(new Set());
|
||||||
|
const pagePlaybackStartedAtRef = useRef<number>(Date.now());
|
||||||
|
|
||||||
const activePage = useMemo(
|
const activePage = useMemo(
|
||||||
() => pages.find((item) => item.id === activePageId) || null,
|
() => pages.find((item) => item.id === activePageId) || null,
|
||||||
[activePageId, pages],
|
[activePageId, pages],
|
||||||
);
|
);
|
||||||
const activePageIndex = useMemo(
|
|
||||||
() => pages.findIndex((item) => item.id === activePageId),
|
|
||||||
[activePageId, pages],
|
|
||||||
);
|
|
||||||
const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
|
const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
|
||||||
if (pages.length <= 1) return ['navigation_next'];
|
|
||||||
if (activePageIndex < 0) return ['navigation_next', 'navigation_prev'];
|
|
||||||
if (activePageIndex <= 0) return ['navigation_next'];
|
|
||||||
if (activePageIndex >= pages.length - 1) return ['navigation_prev'];
|
|
||||||
return ['navigation_next', 'navigation_prev'];
|
return ['navigation_next', 'navigation_prev'];
|
||||||
}, [activePageIndex, pages.length]);
|
}, []);
|
||||||
const pageNameById = useMemo(() => {
|
const pageNameById = useMemo(() => {
|
||||||
const acc: Record<string, string> = {};
|
const acc: Record<string, string> = {};
|
||||||
pages.forEach((page, index) => {
|
pages.forEach((page, index) => {
|
||||||
@ -970,6 +963,25 @@ const ConstructorPage = () => {
|
|||||||
});
|
});
|
||||||
}, [isAuthReady, isLoading, router.isReady]);
|
}, [isAuthReady, isLoading, router.isReady]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if (isLoading || !activePageId) {
|
||||||
|
setCanvasElapsedSec(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pagePlaybackStartedAtRef.current = Date.now();
|
||||||
|
setCanvasElapsedSec(0);
|
||||||
|
|
||||||
|
const intervalId = window.setInterval(() => {
|
||||||
|
const elapsed =
|
||||||
|
(Date.now() - pagePlaybackStartedAtRef.current) / 1000;
|
||||||
|
setCanvasElapsedSec(elapsed > 0 ? elapsed : 0);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => window.clearInterval(intervalId);
|
||||||
|
}, [activePageId, isLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activePage) {
|
if (!activePage) {
|
||||||
setElements([]);
|
setElements([]);
|
||||||
@ -1076,6 +1088,12 @@ const ConstructorPage = () => {
|
|||||||
setSelectedMenuItem('none');
|
setSelectedMenuItem('none');
|
||||||
setSelectedElementId((current) => {
|
setSelectedElementId((current) => {
|
||||||
if (!normalizedElements.length) return '';
|
if (!normalizedElements.length) return '';
|
||||||
|
if (
|
||||||
|
elementIdFromRoute &&
|
||||||
|
normalizedElements.some((element) => element.id === elementIdFromRoute)
|
||||||
|
) {
|
||||||
|
return elementIdFromRoute;
|
||||||
|
}
|
||||||
if (normalizedElements.some((element) => element.id === current))
|
if (normalizedElements.some((element) => element.id === current))
|
||||||
return current;
|
return current;
|
||||||
return '';
|
return '';
|
||||||
@ -1083,7 +1101,7 @@ const ConstructorPage = () => {
|
|||||||
setBackgroundImageUrl(activePage.background_image_url || '');
|
setBackgroundImageUrl(activePage.background_image_url || '');
|
||||||
setBackgroundVideoUrl(activePage.background_video_url || '');
|
setBackgroundVideoUrl(activePage.background_video_url || '');
|
||||||
setBackgroundAudioUrl(activePage.background_audio_url || '');
|
setBackgroundAudioUrl(activePage.background_audio_url || '');
|
||||||
}, [activePage]);
|
}, [activePage, elementIdFromRoute]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allowedNavigationTypes.length !== 1) return;
|
if (allowedNavigationTypes.length !== 1) return;
|
||||||
@ -1573,24 +1591,24 @@ const ConstructorPage = () => {
|
|||||||
element.type === 'navigation_next' ||
|
element.type === 'navigation_next' ||
|
||||||
element.type === 'navigation_prev'
|
element.type === 'navigation_prev'
|
||||||
) {
|
) {
|
||||||
|
if (element.iconUrl) {
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
||||||
|
alt='Navigation icon'
|
||||||
|
className='block h-auto w-auto max-h-[220px] max-w-[220px] object-contain'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const targetPageName = element.targetPageId
|
const targetPageName = element.targetPageId
|
||||||
? pageNameById[element.targetPageId]
|
? pageNameById[element.targetPageId]
|
||||||
: '';
|
: '';
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-start gap-1'>
|
<div className='flex flex-col items-start gap-1'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
{element.iconUrl ? (
|
<span>{element.navLabel}</span>
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
|
||||||
alt='Navigation icon'
|
|
||||||
className='h-4 w-4 object-contain'
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<span>
|
|
||||||
{element.navLabel ||
|
|
||||||
(element.type === 'navigation_next' ? 'Forward' : 'Back')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{targetPageName ? (
|
{targetPageName ? (
|
||||||
<span className='text-[10px] text-gray-500'>
|
<span className='text-[10px] text-gray-500'>
|
||||||
@ -1602,18 +1620,21 @@ const ConstructorPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (element.type === 'tooltip') {
|
if (element.type === 'tooltip') {
|
||||||
|
if (element.iconUrl) {
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
||||||
|
alt='Tooltip icon'
|
||||||
|
className='block h-auto w-auto max-h-[220px] max-w-[220px] object-contain'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-[200px] text-left'>
|
<div className='max-w-[200px] text-left'>
|
||||||
{element.iconUrl ? (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
|
||||||
alt='Tooltip icon'
|
|
||||||
className='mb-1 h-5 w-5 object-contain'
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<p className='text-[11px] font-bold'>
|
<p className='text-[11px] font-bold'>
|
||||||
{element.tooltipTitle || 'Tooltip title'}
|
{element.tooltipTitle}
|
||||||
</p>
|
</p>
|
||||||
<p className='text-[10px] text-gray-600 line-clamp-3'>
|
<p className='text-[10px] text-gray-600 line-clamp-3'>
|
||||||
{element.tooltipText || 'Tooltip text'}
|
{element.tooltipText || 'Tooltip text'}
|
||||||
@ -1623,18 +1644,21 @@ const ConstructorPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (element.type === 'description') {
|
if (element.type === 'description') {
|
||||||
|
if (element.iconUrl) {
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
||||||
|
alt='Description icon'
|
||||||
|
className='block h-auto w-auto max-h-[220px] max-w-[220px] object-contain'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-[220px] text-left'>
|
<div className='max-w-[220px] text-left'>
|
||||||
{element.iconUrl ? (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
|
||||||
alt='Description icon'
|
|
||||||
className='mb-1 h-5 w-5 object-contain'
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<p className='text-[11px] font-bold'>
|
<p className='text-[11px] font-bold'>
|
||||||
{element.descriptionTitle || 'Description title'}
|
{element.descriptionTitle}
|
||||||
</p>
|
</p>
|
||||||
<p className='text-[10px] text-gray-600 line-clamp-4'>
|
<p className='text-[10px] text-gray-600 line-clamp-4'>
|
||||||
{element.descriptionText || 'Description text'}
|
{element.descriptionText || 'Description text'}
|
||||||
@ -1770,6 +1794,20 @@ const ConstructorPage = () => {
|
|||||||
return getElementButtonTitle(element);
|
return getElementButtonTitle(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isElementVisibleOnCanvas = (element: CanvasElement) => {
|
||||||
|
const delay = Number(element.appearDelaySec || 0);
|
||||||
|
if (canvasElapsedSec < delay) return false;
|
||||||
|
|
||||||
|
if (element.appearDurationSec === null || element.appearDurationSec === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Number(element.appearDurationSec);
|
||||||
|
if (!Number.isFinite(duration) || duration <= 0) return true;
|
||||||
|
|
||||||
|
return canvasElapsedSec <= delay + duration;
|
||||||
|
};
|
||||||
|
|
||||||
const canvasBackgroundStyle: React.CSSProperties = {};
|
const canvasBackgroundStyle: React.CSSProperties = {};
|
||||||
const backgroundImageSrc = resolveAssetPlaybackUrl(backgroundImageUrl);
|
const backgroundImageSrc = resolveAssetPlaybackUrl(backgroundImageUrl);
|
||||||
const backgroundVideoSrc = resolveAssetPlaybackUrl(backgroundVideoUrl);
|
const backgroundVideoSrc = resolveAssetPlaybackUrl(backgroundVideoUrl);
|
||||||
@ -1999,27 +2037,43 @@ const ConstructorPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
elements.map((element) => (
|
elements.map((element) => {
|
||||||
<button
|
const shouldRender =
|
||||||
key={element.id}
|
selectedElementId === element.id ||
|
||||||
type='button'
|
isElementVisibleOnCanvas(element);
|
||||||
data-constructor-element-id={element.id}
|
if (!shouldRender) return null;
|
||||||
className={`absolute border rounded px-3 py-2 text-xs font-semibold shadow cursor-move text-left ${
|
|
||||||
selectedElementId === element.id
|
const hasIconDrivenSize =
|
||||||
? 'border-blue-500 bg-blue-50'
|
Boolean(element.iconUrl) &&
|
||||||
: 'border-blue-200 bg-white/95'
|
(element.type === 'navigation_next' ||
|
||||||
}`}
|
element.type === 'navigation_prev' ||
|
||||||
style={{
|
element.type === 'tooltip' ||
|
||||||
left: `${element.xPercent}%`,
|
element.type === 'description');
|
||||||
top: `${element.yPercent}%`,
|
|
||||||
transform: 'translate(-50%, -50%)',
|
return (
|
||||||
}}
|
<button
|
||||||
onMouseDown={(event) => onElementMouseDown(event, element.id)}
|
key={element.id}
|
||||||
onClick={() => selectElementForEdit(element.id)}
|
type='button'
|
||||||
>
|
data-constructor-element-id={element.id}
|
||||||
{renderCanvasElementContent(element)}
|
className={`absolute border rounded text-xs font-semibold shadow cursor-move text-left ${
|
||||||
</button>
|
hasIconDrivenSize ? 'overflow-hidden p-0 leading-none' : 'px-3 py-2'
|
||||||
))
|
} ${
|
||||||
|
selectedElementId === element.id
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-blue-200 bg-white/95'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
left: `${element.xPercent}%`,
|
||||||
|
top: `${element.yPercent}%`,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => onElementMouseDown(event, element.id)}
|
||||||
|
onClick={() => selectElementForEdit(element.id)}
|
||||||
|
>
|
||||||
|
{renderCanvasElementContent(element)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,128 +1,59 @@
|
|||||||
import { mdiChartTimelineVariant, mdiClose, mdiViewDashboard } from '@mdi/js';
|
import { mdiChartTimelineVariant, mdiViewDashboard } from '@mdi/js';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, {
|
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
ReactElement,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import BaseButton from '../../components/BaseButton';
|
import BaseButton from '../../components/BaseButton';
|
||||||
import CardBox from '../../components/CardBox';
|
import CardBox from '../../components/CardBox';
|
||||||
import CardBoxModal from '../../components/CardBoxModal';
|
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||||
import SectionMain from '../../components/SectionMain';
|
import SectionMain from '../../components/SectionMain';
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||||
import { getPageTitle } from '../../config';
|
import { getPageTitle } from '../../config';
|
||||||
import { hasPermission } from '../../helpers/userPermissions';
|
|
||||||
import { useAppSelector } from '../../stores/hooks';
|
|
||||||
|
|
||||||
type ElementSettings = {
|
type TourPage = {
|
||||||
color: string;
|
id: string;
|
||||||
backgroundColor: string;
|
name?: string;
|
||||||
border: string;
|
sort_order?: number;
|
||||||
icon: string;
|
ui_schema_json?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConstructorElement = {
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
label?: string;
|
||||||
|
navLabel?: string;
|
||||||
|
tooltipTitle?: string;
|
||||||
|
descriptionTitle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConstructorSchema = {
|
||||||
|
elements?: ConstructorElement[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProjectElementItem = {
|
type ProjectElementItem = {
|
||||||
id: string;
|
id: string;
|
||||||
|
pageId: string;
|
||||||
|
pageName: string;
|
||||||
elementType: string;
|
elementType: string;
|
||||||
name: string;
|
name: string;
|
||||||
settings: ElementSettings;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type PlatformElementOption = {
|
const parseJsonObject = (value?: unknown): Record<string, any> => {
|
||||||
elementType: string;
|
|
||||||
name: string;
|
|
||||||
defaults: ElementSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PageElementRecord = {
|
|
||||||
id: string;
|
|
||||||
element_type?: string;
|
|
||||||
name?: string;
|
|
||||||
style_json?: string;
|
|
||||||
content_json?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FALLBACK_ELEMENT_TYPES = [
|
|
||||||
'nav_button',
|
|
||||||
'spot',
|
|
||||||
'description',
|
|
||||||
'tooltip',
|
|
||||||
'gallery',
|
|
||||||
'carousel',
|
|
||||||
'logo',
|
|
||||||
'video_player',
|
|
||||||
'popup',
|
|
||||||
];
|
|
||||||
|
|
||||||
const FALLBACK_DEFAULTS_BY_TYPE: Record<string, ElementSettings> = {
|
|
||||||
nav_button: {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
icon: 'mdiArrowRight',
|
|
||||||
},
|
|
||||||
spot: {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#fde68a',
|
|
||||||
border: '1px solid #f59e0b',
|
|
||||||
icon: 'mdiMapMarker',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#f3f4f6',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
icon: 'mdiTextBox',
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
color: '#ffffff',
|
|
||||||
backgroundColor: '#1f2937',
|
|
||||||
border: '1px solid #1f2937',
|
|
||||||
icon: 'mdiTooltipText',
|
|
||||||
},
|
|
||||||
gallery: {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
icon: 'mdiImageMultiple',
|
|
||||||
},
|
|
||||||
carousel: {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
icon: 'mdiViewCarousel',
|
|
||||||
},
|
|
||||||
logo: {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
icon: 'mdiImage',
|
|
||||||
},
|
|
||||||
video_player: {
|
|
||||||
color: '#ffffff',
|
|
||||||
backgroundColor: '#111827',
|
|
||||||
border: '1px solid #111827',
|
|
||||||
icon: 'mdiPlayCircle',
|
|
||||||
},
|
|
||||||
popup: {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
icon: 'mdiOpenInNew',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseJsonObject = (value?: string): Record<string, any> => {
|
|
||||||
if (!value) return {};
|
if (!value) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(value);
|
if (typeof value === 'string') {
|
||||||
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
const parsed = JSON.parse(value);
|
||||||
} catch {
|
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return value as Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse page schema JSON on pages elements list:', error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -133,23 +64,21 @@ const toElementLabel = (value: string) =>
|
|||||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
const createLocalId = () => {
|
const getElementName = (element: ConstructorElement) => {
|
||||||
if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
|
if (element.type === 'navigation_next' || element.type === 'navigation_prev') {
|
||||||
return window.crypto.randomUUID();
|
return String(element.navLabel || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return `pe_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
if (element.type === 'tooltip') {
|
||||||
};
|
return String(element.tooltipTitle || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeSettings = (
|
if (element.type === 'description') {
|
||||||
settings: Partial<ElementSettings> | undefined,
|
return String(element.descriptionTitle || '').trim();
|
||||||
fallback: ElementSettings,
|
}
|
||||||
): ElementSettings => ({
|
|
||||||
color: settings?.color || fallback.color,
|
return String(element.label || '').trim();
|
||||||
backgroundColor: settings?.backgroundColor || fallback.backgroundColor,
|
};
|
||||||
border: settings?.border || fallback.border,
|
|
||||||
icon: settings?.icon || fallback.icon,
|
|
||||||
});
|
|
||||||
|
|
||||||
const PagesElementsListPage = () => {
|
const PagesElementsListPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -159,75 +88,16 @@ const PagesElementsListPage = () => {
|
|||||||
return String(value || '');
|
return String(value || '');
|
||||||
}, [router.query.projectId]);
|
}, [router.query.projectId]);
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
|
||||||
const hasCreatePermission = hasPermission(
|
|
||||||
currentUser,
|
|
||||||
'CREATE_PAGE_ELEMENTS',
|
|
||||||
);
|
|
||||||
const hasUpdatePermission = hasPermission(
|
|
||||||
currentUser,
|
|
||||||
'UPDATE_PAGE_ELEMENTS',
|
|
||||||
);
|
|
||||||
const hasDeletePermission = hasPermission(
|
|
||||||
currentUser,
|
|
||||||
'DELETE_PAGE_ELEMENTS',
|
|
||||||
);
|
|
||||||
|
|
||||||
const [projectName, setProjectName] = useState('');
|
const [projectName, setProjectName] = useState('');
|
||||||
const [isLoadingProject, setIsLoadingProject] = useState(false);
|
const [isLoadingProject, setIsLoadingProject] = useState(false);
|
||||||
const [isLoadingElements, setIsLoadingElements] = useState(false);
|
const [isLoadingElements, setIsLoadingElements] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const [projectElements, setProjectElements] = useState<ProjectElementItem[]>([]);
|
||||||
const [themeConfig, setThemeConfig] = useState<Record<string, any>>({});
|
|
||||||
const [selectedElements, setSelectedElements] = useState<
|
|
||||||
ProjectElementItem[]
|
|
||||||
>([]);
|
|
||||||
const [platformElements, setPlatformElements] = useState<
|
|
||||||
PlatformElementOption[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const [isAddDropdownOpen, setIsAddDropdownOpen] = useState(false);
|
|
||||||
const [newElementType, setNewElementType] = useState('');
|
|
||||||
|
|
||||||
const [isSettingsModalActive, setIsSettingsModalActive] = useState(false);
|
|
||||||
const [activeElement, setActiveElement] = useState<ProjectElementItem | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [elementName, setElementName] = useState('');
|
|
||||||
const [color, setColor] = useState('');
|
|
||||||
const [backgroundColor, setBackgroundColor] = useState('');
|
|
||||||
const [border, setBorder] = useState('');
|
|
||||||
const [icon, setIcon] = useState('');
|
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
// Check if projects exist and redirect if not
|
|
||||||
useEffect(() => {
|
|
||||||
const checkProjects = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('/projects/autocomplete?limit=1');
|
|
||||||
const projects = Array.isArray(response?.data) ? response.data : [];
|
|
||||||
if (projects.length === 0) {
|
|
||||||
toast('Please create a project first', {
|
|
||||||
type: 'info',
|
|
||||||
position: 'bottom-center',
|
|
||||||
});
|
|
||||||
router.replace('/projects/projects-new');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to check projects:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
checkProjects();
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
if (!routeProjectId) {
|
if (!routeProjectId) {
|
||||||
setProjectName('');
|
setProjectName('');
|
||||||
setThemeConfig({});
|
setProjectElements([]);
|
||||||
setSelectedElements([]);
|
|
||||||
setPlatformElements([]);
|
|
||||||
setNewElementType('');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,92 +106,51 @@ const PagesElementsListPage = () => {
|
|||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [projectResponse, pageElementsResponse] = await Promise.all([
|
const [projectResponse, pagesResponse] = await Promise.all([
|
||||||
axios.get(`/projects/${routeProjectId}`),
|
axios.get(`/projects/${routeProjectId}`),
|
||||||
axios.get('/page_elements?limit=1000&page=0&sort=desc&field=updatedAt'),
|
axios.get(
|
||||||
|
`/tour_pages?limit=500&page=0&sort=asc&field=sort_order&project=${routeProjectId}`,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const project = projectResponse?.data || {};
|
const project = projectResponse?.data || {};
|
||||||
setProjectName(project?.name || '');
|
setProjectName(project?.name || '');
|
||||||
|
|
||||||
const parsedThemeConfig = parseJsonObject(project?.theme_config_json);
|
const pageRows: TourPage[] = Array.isArray(pagesResponse?.data?.rows)
|
||||||
const rawProjectElements = Array.isArray(parsedThemeConfig?.pageElements)
|
? pagesResponse.data.rows
|
||||||
? parsedThemeConfig.pageElements
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const normalizedProjectElements: ProjectElementItem[] = rawProjectElements
|
const items: ProjectElementItem[] = [];
|
||||||
.filter((item: any) => item && item.elementType)
|
|
||||||
.map((item: any) => {
|
|
||||||
const elementType = String(item.elementType);
|
|
||||||
const defaults = FALLBACK_DEFAULTS_BY_TYPE[elementType] || {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
icon: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
pageRows.forEach((page, pageIndex) => {
|
||||||
id: String(item.id || createLocalId()),
|
const schema = parseJsonObject(page.ui_schema_json) as ConstructorSchema;
|
||||||
|
const elements = Array.isArray(schema.elements) ? schema.elements : [];
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
const elementType = String(element?.type || '').trim();
|
||||||
|
const elementId = String(element?.id || '').trim();
|
||||||
|
if (!elementType || !elementId) return;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id: elementId,
|
||||||
|
pageId: String(page.id),
|
||||||
|
pageName: String(page.name || `Page ${pageIndex + 1}`),
|
||||||
elementType,
|
elementType,
|
||||||
name: String(item.name || toElementLabel(elementType)),
|
name: getElementName(element),
|
||||||
settings: normalizeSettings(item.settings || {}, defaults),
|
});
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = Array.isArray(pageElementsResponse?.data?.rows)
|
|
||||||
? pageElementsResponse.data.rows
|
|
||||||
: [];
|
|
||||||
const optionsMap = new Map<string, PlatformElementOption>();
|
|
||||||
|
|
||||||
FALLBACK_ELEMENT_TYPES.forEach((type) => {
|
|
||||||
optionsMap.set(type, {
|
|
||||||
elementType: type,
|
|
||||||
name: toElementLabel(type),
|
|
||||||
defaults: FALLBACK_DEFAULTS_BY_TYPE[type],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
rows.forEach((row: PageElementRecord) => {
|
setProjectElements(items);
|
||||||
const elementType = String(row.element_type || '').trim();
|
|
||||||
if (!elementType || optionsMap.has(elementType)) return;
|
|
||||||
|
|
||||||
const rowStyle = parseJsonObject(row.style_json);
|
|
||||||
const rowContent = parseJsonObject(row.content_json);
|
|
||||||
const fallback = FALLBACK_DEFAULTS_BY_TYPE[elementType] || {
|
|
||||||
color: '#111827',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
icon: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
optionsMap.set(elementType, {
|
|
||||||
elementType,
|
|
||||||
name: row.name || toElementLabel(elementType),
|
|
||||||
defaults: {
|
|
||||||
color: String(rowStyle.color || fallback.color),
|
|
||||||
backgroundColor: String(
|
|
||||||
rowStyle.backgroundColor || fallback.backgroundColor,
|
|
||||||
),
|
|
||||||
border: String(rowStyle.border || fallback.border),
|
|
||||||
icon: String(rowContent.icon || fallback.icon),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalizedOptions = Array.from(optionsMap.values());
|
|
||||||
setThemeConfig(parsedThemeConfig);
|
|
||||||
setSelectedElements(normalizedProjectElements);
|
|
||||||
setPlatformElements(normalizedOptions);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message =
|
const message =
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
error?.message ||
|
error?.message ||
|
||||||
'Failed to load pages elements.';
|
'Failed to load pages elements.';
|
||||||
setErrorMessage(message);
|
setErrorMessage(message);
|
||||||
console.error('Failed to load pages elements list:', error);
|
console.error('Failed to load project elements from constructor pages:', error);
|
||||||
setThemeConfig({});
|
setProjectName('');
|
||||||
setSelectedElements([]);
|
setProjectElements([]);
|
||||||
setPlatformElements([]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingProject(false);
|
setIsLoadingProject(false);
|
||||||
setIsLoadingElements(false);
|
setIsLoadingElements(false);
|
||||||
@ -332,190 +161,23 @@ const PagesElementsListPage = () => {
|
|||||||
loadData();
|
loadData();
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
const selectedTypes = useMemo(
|
|
||||||
() => new Set(selectedElements.map((item) => item.elementType)),
|
|
||||||
[selectedElements],
|
|
||||||
);
|
|
||||||
|
|
||||||
const availableToAdd = useMemo(
|
|
||||||
() =>
|
|
||||||
platformElements.filter((item) => !selectedTypes.has(item.elementType)),
|
|
||||||
[platformElements, selectedTypes],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!availableToAdd.length) {
|
|
||||||
setNewElementType('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!availableToAdd.some((item) => item.elementType === newElementType)) {
|
|
||||||
setNewElementType(availableToAdd[0].elementType);
|
|
||||||
}
|
|
||||||
}, [availableToAdd, newElementType]);
|
|
||||||
|
|
||||||
const saveProjectElements = useCallback(
|
|
||||||
async (elements: ProjectElementItem[]) => {
|
|
||||||
if (!routeProjectId) return;
|
|
||||||
|
|
||||||
const nextThemeConfig = {
|
|
||||||
...(themeConfig || {}),
|
|
||||||
pageElements: elements.map((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
elementType: item.elementType,
|
|
||||||
name: item.name,
|
|
||||||
settings: item.settings,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
await axios.put(`/projects/${routeProjectId}`, {
|
|
||||||
id: routeProjectId,
|
|
||||||
data: {
|
|
||||||
theme_config_json: JSON.stringify(nextThemeConfig),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setThemeConfig(nextThemeConfig);
|
|
||||||
},
|
|
||||||
[routeProjectId, themeConfig],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAddElement = async () => {
|
|
||||||
if (!hasCreatePermission) return;
|
|
||||||
|
|
||||||
if (!routeProjectId) {
|
|
||||||
setErrorMessage('Please select a project first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newElementType || selectedTypes.has(newElementType)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedOption = platformElements.find(
|
|
||||||
(item) => item.elementType === newElementType,
|
|
||||||
);
|
|
||||||
if (!selectedOption) {
|
|
||||||
setErrorMessage('Selected element type is not available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSaving(true);
|
|
||||||
setErrorMessage('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updatedElements = [
|
|
||||||
...selectedElements,
|
|
||||||
{
|
|
||||||
id: createLocalId(),
|
|
||||||
elementType: selectedOption.elementType,
|
|
||||||
name: selectedOption.name,
|
|
||||||
settings: { ...selectedOption.defaults },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await saveProjectElements(updatedElements);
|
|
||||||
setSelectedElements(updatedElements);
|
|
||||||
setIsAddDropdownOpen(false);
|
|
||||||
} catch (error: any) {
|
|
||||||
const message =
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error?.message ||
|
|
||||||
'Failed to add element.';
|
|
||||||
setErrorMessage(message);
|
|
||||||
console.error('Failed to add project element:', error);
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveElement = async (item: ProjectElementItem) => {
|
|
||||||
if (!hasDeletePermission) return;
|
|
||||||
|
|
||||||
const confirmed = window.confirm(
|
|
||||||
`Remove ${item.name || toElementLabel(item.elementType)} from this project?`,
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
setIsSaving(true);
|
|
||||||
setErrorMessage('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updatedElements = selectedElements.filter(
|
|
||||||
(existing) => existing.id !== item.id,
|
|
||||||
);
|
|
||||||
await saveProjectElements(updatedElements);
|
|
||||||
setSelectedElements(updatedElements);
|
|
||||||
} catch (error: any) {
|
|
||||||
const message =
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error?.message ||
|
|
||||||
'Failed to remove element.';
|
|
||||||
setErrorMessage(message);
|
|
||||||
console.error('Failed to remove project element:', error);
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openSettings = (item: ProjectElementItem) => {
|
|
||||||
if (!hasUpdatePermission) return;
|
|
||||||
|
|
||||||
setActiveElement(item);
|
|
||||||
setElementName(item.name || toElementLabel(item.elementType));
|
|
||||||
setColor(item.settings.color);
|
|
||||||
setBackgroundColor(item.settings.backgroundColor);
|
|
||||||
setBorder(item.settings.border);
|
|
||||||
setIcon(item.settings.icon);
|
|
||||||
setIsSettingsModalActive(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeSettings = () => {
|
|
||||||
setIsSettingsModalActive(false);
|
|
||||||
setActiveElement(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSettings = async () => {
|
|
||||||
if (!activeElement || !hasUpdatePermission) return;
|
|
||||||
|
|
||||||
setIsSaving(true);
|
|
||||||
setErrorMessage('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updatedElements = selectedElements.map((item) => {
|
|
||||||
if (item.id !== activeElement.id) return item;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
name: elementName,
|
|
||||||
settings: {
|
|
||||||
color,
|
|
||||||
backgroundColor,
|
|
||||||
border,
|
|
||||||
icon,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
await saveProjectElements(updatedElements);
|
|
||||||
setSelectedElements(updatedElements);
|
|
||||||
closeSettings();
|
|
||||||
} catch (error: any) {
|
|
||||||
const message =
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error?.message ||
|
|
||||||
'Failed to save element settings.';
|
|
||||||
setErrorMessage(message);
|
|
||||||
console.error('Failed to update project element settings:', error);
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const constructorHref = routeProjectId
|
const constructorHref = routeProjectId
|
||||||
? `/constructor?projectId=${routeProjectId}`
|
? `/constructor?projectId=${routeProjectId}`
|
||||||
: '/constructor';
|
: '/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 (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@ -540,51 +202,6 @@ const PagesElementsListPage = () => {
|
|||||||
className='mb-6'
|
className='mb-6'
|
||||||
cardBoxClassName='flex flex-wrap items-start gap-3'
|
cardBoxClassName='flex flex-wrap items-start gap-3'
|
||||||
>
|
>
|
||||||
{hasCreatePermission && (
|
|
||||||
<div className='relative'>
|
|
||||||
<BaseButton
|
|
||||||
color='info'
|
|
||||||
label='Add element'
|
|
||||||
onClick={() => setIsAddDropdownOpen((prev) => !prev)}
|
|
||||||
disabled={isSaving}
|
|
||||||
/>
|
|
||||||
{isAddDropdownOpen && (
|
|
||||||
<div className='absolute z-20 mt-2 w-72 rounded border border-gray-200 bg-white p-3 shadow-lg dark:bg-dark-900 dark:border-dark-700'>
|
|
||||||
<select
|
|
||||||
className='mb-2 w-full rounded border border-gray-300 px-2 py-2 dark:bg-dark-800'
|
|
||||||
value={newElementType}
|
|
||||||
onChange={(event) => setNewElementType(event.target.value)}
|
|
||||||
disabled={isSaving || availableToAdd.length === 0}
|
|
||||||
>
|
|
||||||
{platformElements.map((item) => (
|
|
||||||
<option
|
|
||||||
key={item.elementType}
|
|
||||||
value={item.elementType}
|
|
||||||
disabled={selectedTypes.has(item.elementType)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
{selectedTypes.has(item.elementType)
|
|
||||||
? ' (selected)'
|
|
||||||
: ''}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<BaseButton
|
|
||||||
color='success'
|
|
||||||
label={isSaving ? 'Adding...' : 'Add'}
|
|
||||||
onClick={handleAddElement}
|
|
||||||
disabled={
|
|
||||||
isSaving ||
|
|
||||||
!availableToAdd.length ||
|
|
||||||
!newElementType ||
|
|
||||||
selectedTypes.has(newElementType)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color='lightDark'
|
color='lightDark'
|
||||||
icon={mdiViewDashboard}
|
icon={mdiViewDashboard}
|
||||||
@ -602,110 +219,32 @@ const PagesElementsListPage = () => {
|
|||||||
|
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<h3 className='mb-3 text-lg font-semibold'>
|
<h3 className='mb-3 text-lg font-semibold'>
|
||||||
Selected pages elements
|
Project elements from constructor pages
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{isLoadingElements ? (
|
{isLoadingElements ? (
|
||||||
<p className='text-sm text-gray-500'>Loading elements...</p>
|
<p className='text-sm text-gray-500'>Loading elements...</p>
|
||||||
) : selectedElements.length === 0 ? (
|
) : projectElements.length === 0 ? (
|
||||||
<p className='text-sm text-gray-500'>No elements selected yet.</p>
|
<p className='text-sm text-gray-500'>No constructor elements found yet.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
{selectedElements.map((item) => (
|
{projectElements.map((item) => (
|
||||||
<div
|
<button
|
||||||
key={item.id}
|
key={`${item.pageId}_${item.id}`}
|
||||||
className='flex items-center justify-between gap-3 rounded border border-gray-200 px-3 py-2 dark:border-dark-700'
|
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)}
|
||||||
>
|
>
|
||||||
<button
|
<p className='text-sm font-semibold'>{item.name}</p>
|
||||||
type='button'
|
<p className='text-xs text-gray-500'>
|
||||||
className='flex-1 text-left'
|
{item.pageName} • {toElementLabel(item.elementType)}
|
||||||
onClick={() => openSettings(item)}
|
</p>
|
||||||
disabled={!hasUpdatePermission || isSaving}
|
</button>
|
||||||
>
|
|
||||||
<p className='text-sm font-semibold'>
|
|
||||||
{item.name || toElementLabel(item.elementType)}
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-gray-500'>
|
|
||||||
{toElementLabel(item.elementType)} • icon:{' '}
|
|
||||||
{item.settings.icon || '-'}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{hasDeletePermission ? (
|
|
||||||
<BaseButton
|
|
||||||
color='danger'
|
|
||||||
icon={mdiClose}
|
|
||||||
small
|
|
||||||
roundedFull
|
|
||||||
onClick={() => handleRemoveElement(item)}
|
|
||||||
disabled={isSaving}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|
||||||
<CardBoxModal
|
|
||||||
title={
|
|
||||||
activeElement
|
|
||||||
? `Edit ${toElementLabel(activeElement.elementType)}`
|
|
||||||
: 'Element settings'
|
|
||||||
}
|
|
||||||
buttonColor='info'
|
|
||||||
buttonLabel={isSaving ? 'Saving...' : 'Save settings'}
|
|
||||||
isActive={isSettingsModalActive}
|
|
||||||
onConfirm={saveSettings}
|
|
||||||
onCancel={closeSettings}
|
|
||||||
isConfirmDisabled={isSaving || !hasUpdatePermission}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-sm font-semibold'>Name</label>
|
|
||||||
<input
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-2 dark:bg-dark-800 dark:border-dark-700'
|
|
||||||
value={elementName}
|
|
||||||
onChange={(event) => setElementName(event.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-sm font-semibold'>Color</label>
|
|
||||||
<input
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-2 dark:bg-dark-800 dark:border-dark-700'
|
|
||||||
value={color}
|
|
||||||
onChange={(event) => setColor(event.target.value)}
|
|
||||||
placeholder='#111827'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-sm font-semibold'>Background</label>
|
|
||||||
<input
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-2 dark:bg-dark-800 dark:border-dark-700'
|
|
||||||
value={backgroundColor}
|
|
||||||
onChange={(event) => setBackgroundColor(event.target.value)}
|
|
||||||
placeholder='#ffffff'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-sm font-semibold'>Border</label>
|
|
||||||
<input
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-2 dark:bg-dark-800 dark:border-dark-700'
|
|
||||||
value={border}
|
|
||||||
onChange={(event) => setBorder(event.target.value)}
|
|
||||||
placeholder='1px solid #d1d5db'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-sm font-semibold'>Icon</label>
|
|
||||||
<input
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-2 dark:bg-dark-800 dark:border-dark-700'
|
|
||||||
value={icon}
|
|
||||||
onChange={(event) => setIcon(event.target.value)}
|
|
||||||
placeholder='mdiStar'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardBoxModal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
import ConstructorPage from '../constructor';
|
||||||
|
|
||||||
|
export default ConstructorPage;
|
||||||
Loading…
x
Reference in New Issue
Block a user