Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dd206905d |
BIN
assets/pasted-20260124-131548-a94a5f8c.png
Normal file
BIN
assets/pasted-20260124-131548-a94a5f8c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 543 KiB |
@ -1,16 +1,11 @@
|
||||
import React from 'react'
|
||||
import { mdiLogout, mdiClose } from '@mdi/js'
|
||||
import { mdiClose } from '@mdi/js'
|
||||
import BaseIcon from './BaseIcon'
|
||||
import AsideMenuList from './AsideMenuList'
|
||||
import { MenuAsideItem } from '../interfaces'
|
||||
import { useAppSelector } from '../stores/hooks'
|
||||
import Link from 'next/link';
|
||||
|
||||
import { useAppDispatch } from '../stores/hooks';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { useAppSelector, useAppDispatch } from '../stores/hooks'
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
type Props = {
|
||||
menu: MenuAsideItem[]
|
||||
className?: string
|
||||
@ -32,21 +27,18 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
||||
const dispatch = useAppDispatch();
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const organizationsId = currentUser?.organizations?.id;
|
||||
const [organizations, setOrganizations] = React.useState(null);
|
||||
|
||||
const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => {
|
||||
try {
|
||||
const response = await axios.get('/org-for-auth');
|
||||
setOrganizations(response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error.response);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
const [organizations, setOrganizations] = React.useState<any[] | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchOrganizations());
|
||||
const fetchOrganizations = async () => {
|
||||
try {
|
||||
const response = await axios.get('/org-for-auth');
|
||||
setOrganizations(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
fetchOrganizations();
|
||||
}, [dispatch]);
|
||||
|
||||
let organizationName = organizations?.find(item => item.id === organizationsId)?.name;
|
||||
@ -91,4 +83,4 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
|
||||
}
|
||||
|
||||
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
||||
}
|
||||
}
|
||||
684
frontend/src/components/Pages/PageBuilder.tsx
Normal file
684
frontend/src/components/Pages/PageBuilder.tsx
Normal file
@ -0,0 +1,684 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { fetch as fetchPage } from '../../stores/pages/pagesSlice';
|
||||
import { fetch as fetchLayers, update as updateLayer, create as createLayer, deleteItem as removeLayerAction } from '../../stores/layers/layersSlice';
|
||||
import BaseIcon from '../BaseIcon';
|
||||
import {
|
||||
mdiCalendar,
|
||||
mdiCardOutline,
|
||||
mdiCellphone,
|
||||
mdiChevronDown,
|
||||
mdiCloudUploadOutline,
|
||||
mdiCursorDefaultClick,
|
||||
mdiCursorDefaultClickOutline,
|
||||
mdiFilterVariant,
|
||||
mdiFlash,
|
||||
mdiFlashOutline,
|
||||
mdiFolderImage,
|
||||
mdiFormatText,
|
||||
mdiHelpCircleOutline,
|
||||
mdiImageOutline,
|
||||
mdiLayers,
|
||||
mdiMenu,
|
||||
mdiMinus,
|
||||
mdiMonitor,
|
||||
mdiPencil,
|
||||
mdiPlay,
|
||||
mdiPlus,
|
||||
mdiSquareOutline,
|
||||
mdiStar,
|
||||
mdiTablet,
|
||||
mdiVectorSquare
|
||||
} from '@mdi/js';
|
||||
import LoadingSpinner from '../LoadingSpinner';
|
||||
import CardBox from '../CardBox';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface PageBuilderProps {
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
const WIDGETS = [
|
||||
{ type: 'frame', label: 'Frame', icon: mdiVectorSquare, description: 'Basic container' },
|
||||
{ type: 'text', label: 'Text', icon: mdiFormatText, description: 'Text element' },
|
||||
{ type: 'button', label: 'Button', icon: mdiCursorDefaultClick, description: 'Interactive button' },
|
||||
{ type: 'card', label: 'Card', icon: mdiCardOutline, description: 'Styled card container' },
|
||||
{ type: 'image', label: 'Image', icon: mdiImageOutline, description: 'Image placeholder' },
|
||||
{ type: 'calendar', label: 'Calendar', icon: mdiCalendar, description: 'Event calendar' },
|
||||
];
|
||||
|
||||
const PageBuilder: React.FC<PageBuilderProps> = ({ pageId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { pages: page, isFetching: isFetchingPage } = useAppSelector((state) => state.pages);
|
||||
const { layers: allLayers, isFetching: isFetchingLayers } = useAppSelector((state) => state.layers);
|
||||
|
||||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const [localLayer, setLocalLayer] = useState<any>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sidebarTab, setSidebarTab] = useState<'layers' | 'widgets' | 'assets'>('layers');
|
||||
const [activeTabRight, setActiveTabRight] = useState<'style' | 'settings' | 'interactions'>('style');
|
||||
const [viewportWidth, setViewportWidth] = useState(1440);
|
||||
|
||||
const dragStartPos = useRef({ x: 0, y: 0 });
|
||||
const initialLayerPos = useRef({ x: 0, y: 0 });
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (pageId) {
|
||||
dispatch(fetchPage({ id: pageId }));
|
||||
dispatch(fetchLayers({}));
|
||||
}
|
||||
}, [pageId, dispatch]);
|
||||
|
||||
const pageLayers = Array.isArray(allLayers)
|
||||
? [...allLayers].filter((l: any) => l.pageId === pageId || l.page?.id === pageId)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
: [];
|
||||
|
||||
const selectedLayerFromStore = pageLayers.find((l: any) => l.id === selectedLayerId);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedLayerFromStore) {
|
||||
setLocalLayer({
|
||||
...selectedLayerFromStore,
|
||||
parsedProps: selectedLayerFromStore.props ? JSON.parse(selectedLayerFromStore.props) : {}
|
||||
});
|
||||
} else {
|
||||
setLocalLayer(null);
|
||||
}
|
||||
}, [selectedLayerId, selectedLayerFromStore]);
|
||||
|
||||
const handlePropertyChange = (key: string, value: any, isProp = true) => {
|
||||
if (!localLayer) return;
|
||||
|
||||
if (isProp) {
|
||||
setLocalLayer({
|
||||
...localLayer,
|
||||
parsedProps: {
|
||||
...localLayer.parsedProps,
|
||||
[key]: value
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setLocalLayer({
|
||||
...localLayer,
|
||||
[key]: value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const saveLayer = useCallback(async (layerToSave: any) => {
|
||||
if (!layerToSave) return;
|
||||
const { parsedProps, ...rest } = layerToSave;
|
||||
await dispatch(updateLayer({
|
||||
id: rest.id,
|
||||
data: {
|
||||
...rest,
|
||||
props: JSON.stringify(parsedProps)
|
||||
}
|
||||
}));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (localLayer && JSON.stringify(localLayer) !== JSON.stringify({
|
||||
...selectedLayerFromStore,
|
||||
parsedProps: selectedLayerFromStore.props ? JSON.parse(selectedLayerFromStore.props) : {}
|
||||
})) {
|
||||
saveLayer(localLayer);
|
||||
}
|
||||
};
|
||||
|
||||
const addNewLayer = async (type: string, x = 50, y = 50) => {
|
||||
const defaultProps = {
|
||||
x,
|
||||
y,
|
||||
width: type === 'calendar' ? 500 : type === 'card' ? 300 : type === 'image' ? 300 : 200,
|
||||
height: type === 'calendar' ? 400 : type === 'card' ? 200 : type === 'image' ? 200 : (type === 'text' ? 40 : 200),
|
||||
backgroundColor: type === 'frame' ? '#ffffff' : type === 'button' ? '#0070f3' : type === 'card' ? '#ffffff' : 'transparent',
|
||||
color: type === 'button' ? '#ffffff' : '#000000',
|
||||
fontSize: '16px',
|
||||
borderRadius: type === 'button' || type === 'card' ? '8px' : '0px',
|
||||
padding: '16px'
|
||||
};
|
||||
|
||||
const newLayerData = {
|
||||
name: `New ${type}`,
|
||||
layer_type: type,
|
||||
pageId: pageId,
|
||||
props: JSON.stringify(defaultProps),
|
||||
order: pageLayers.length,
|
||||
visible: true
|
||||
};
|
||||
|
||||
const result: any = await dispatch(createLayer(newLayerData));
|
||||
if (result.payload?.id) {
|
||||
setSelectedLayerId(result.payload.id);
|
||||
dispatch(fetchLayers({}));
|
||||
}
|
||||
};
|
||||
|
||||
const removeLayer = async (id: string) => {
|
||||
await dispatch(removeLayerAction(id));
|
||||
setSelectedLayerId(null);
|
||||
dispatch(fetchLayers({}));
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent, layer: any) => {
|
||||
e.stopPropagation();
|
||||
if (selectedLayerId !== layer.id) {
|
||||
setSelectedLayerId(layer.id);
|
||||
}
|
||||
|
||||
setIsDragging(true);
|
||||
dragStartPos.current = { x: e.clientX, y: e.clientY };
|
||||
const props = layer.props ? JSON.parse(layer.props) : {};
|
||||
initialLayerPos.current = { x: props.x || 0, y: props.y || 0 };
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDragging || !localLayer) return;
|
||||
|
||||
const dx = (e.clientX - dragStartPos.current.x) / (zoom / 100);
|
||||
const dy = (e.clientY - dragStartPos.current.y) / (zoom / 100);
|
||||
|
||||
setLocalLayer({
|
||||
...localLayer,
|
||||
parsedProps: {
|
||||
...localLayer.parsedProps,
|
||||
x: Math.round(initialLayerPos.current.x + dx),
|
||||
y: Math.round(initialLayerPos.current.y + dy)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (isDragging) {
|
||||
setIsDragging(false);
|
||||
handleBlur();
|
||||
}
|
||||
};
|
||||
|
||||
const handleWidgetDragStart = (e: React.DragEvent, type: string) => {
|
||||
e.dataTransfer.setData('widgetType', type);
|
||||
};
|
||||
|
||||
const handleCanvasDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const type = e.dataTransfer.getData('widgetType');
|
||||
if (!type || !canvasRef.current) return;
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / (zoom / 100);
|
||||
const y = (e.clientY - rect.top) / (zoom / 100);
|
||||
|
||||
addNewLayer(type, Math.round(x), Math.round(y));
|
||||
};
|
||||
|
||||
if (isFetchingPage || isFetchingLayers) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-[#000000] text-gray-300 font-sans overflow-hidden select-none">
|
||||
|
||||
{/* --- TOP NAVIGATION BAR --- */}
|
||||
<div className="h-12 border-b border-white/5 bg-[#111111] flex items-center justify-between px-3 z-50">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Link href="/dashboard" className="p-2 hover:bg-white/5 rounded-md transition-colors mr-2">
|
||||
<BaseIcon path={mdiMenu} size={20} className="text-gray-400" />
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center space-x-1 px-1">
|
||||
<button className="flex items-center space-x-2 px-3 py-1.5 rounded-md bg-white/5 text-xs font-medium text-white">
|
||||
<span>Design</span>
|
||||
<BaseIcon path={mdiChevronDown} size={14} className="opacity-50" />
|
||||
</button>
|
||||
<button className="px-3 py-1.5 rounded-md hover:bg-white/5 text-xs font-medium text-gray-400 transition-colors">CMS</button>
|
||||
<button className="flex items-center space-x-2 px-3 py-1.5 rounded-md hover:bg-white/5 text-xs font-medium text-gray-400 transition-colors">
|
||||
<BaseIcon path={mdiStar} size={14} className="text-blue-400" />
|
||||
<span>App Gen</span>
|
||||
</button>
|
||||
<button className="px-3 py-1.5 rounded-md hover:bg-white/5 text-xs font-medium text-gray-400 transition-colors">Insights</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-1/2 -translate-x-1/2 flex items-center space-x-2 text-[11px] text-gray-500 font-medium">
|
||||
{localLayer ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-blue-400">{localLayer.name}</span>
|
||||
<span className="opacity-30">|</span>
|
||||
<span>{localLayer.layer_type}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>No element selected</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center bg-white/5 p-0.5 rounded-lg border border-white/5">
|
||||
<button onClick={() => setViewportWidth(1440)} className={`p-1.5 rounded-md transition-all ${viewportWidth === 1440 ? 'bg-white/10 text-white' : 'text-gray-500 hover:text-gray-300'}`}>
|
||||
<BaseIcon path={mdiMonitor} size={16} />
|
||||
</button>
|
||||
<button onClick={() => setViewportWidth(768)} className={`p-1.5 rounded-md transition-all ${viewportWidth === 768 ? 'bg-white/10 text-white' : 'text-gray-500 hover:text-gray-300'}`}>
|
||||
<BaseIcon path={mdiTablet} size={16} />
|
||||
</button>
|
||||
<button onClick={() => setViewportWidth(375)} className={`p-1.5 rounded-md transition-all ${viewportWidth === 375 ? 'bg-white/10 text-white' : 'text-gray-500 hover:text-gray-300'}`}>
|
||||
<BaseIcon path={mdiCellphone} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center bg-white/5 rounded-lg px-2 py-1 h-8 border border-white/5">
|
||||
<button onClick={() => setZoom(z => Math.max(10, z - 10))} className="p-1 hover:text-white transition-colors">
|
||||
<BaseIcon path={mdiMinus} size={12} />
|
||||
</button>
|
||||
<span className="text-[10px] font-mono w-10 text-center">{zoom}%</span>
|
||||
<button onClick={() => setZoom(z => Math.min(400, z + 10))} className="p-1 hover:text-white transition-colors">
|
||||
<BaseIcon path={mdiPlus} size={12} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="p-2 hover:bg-white/5 rounded-md text-gray-400">
|
||||
<BaseIcon path={mdiPlay} size={18} />
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-xs font-bold text-gray-300 hover:text-white transition-colors">Share</button>
|
||||
<button className="bg-white text-black px-4 py-1.5 rounded-md text-xs font-bold hover:bg-gray-200 transition-all shadow-lg shadow-white/5">
|
||||
Publish
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-grow overflow-hidden">
|
||||
|
||||
{/* --- LEFT SIDEBAR (Icon Rail + Panel) --- */}
|
||||
<div className="flex bg-[#111111] border-r border-white/5">
|
||||
{/* Icon Rail */}
|
||||
<div className="w-12 border-r border-white/5 flex flex-col items-center py-4 space-y-4">
|
||||
<button onClick={() => setSidebarTab('layers')} className={`p-2 rounded-lg transition-colors ${sidebarTab === 'layers' ? 'text-blue-500 bg-blue-500/10' : 'text-gray-500 hover:text-gray-300'}`}>
|
||||
<BaseIcon path={mdiLayers} size={20} />
|
||||
</button>
|
||||
<button onClick={() => setSidebarTab('widgets')} className={`p-2 rounded-lg transition-colors ${sidebarTab === 'widgets' ? 'text-blue-500 bg-blue-500/10' : 'text-gray-500 hover:text-gray-300'}`}>
|
||||
<BaseIcon path={mdiPencil} size={20} />
|
||||
</button>
|
||||
<button onClick={() => setSidebarTab('assets')} className={`p-2 rounded-lg transition-colors ${sidebarTab === 'assets' ? 'text-blue-500 bg-blue-500/10' : 'text-gray-500 hover:text-gray-300'}`}>
|
||||
<BaseIcon path={mdiFolderImage} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content Panel */}
|
||||
<div className="w-60 flex flex-col bg-[#111111]">
|
||||
<div className="p-4 border-b border-white/5 flex items-center justify-between">
|
||||
<span className="text-[11px] font-bold uppercase tracking-widest text-gray-500">
|
||||
{sidebarTab === 'layers' ? 'Navigator' : sidebarTab === 'widgets' ? 'Components' : 'Assets'}
|
||||
</span>
|
||||
<BaseIcon path={mdiFilterVariant} size={14} className="text-gray-600 cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
{sidebarTab === 'widgets' ? (
|
||||
<div className="p-3 grid grid-cols-2 gap-2">
|
||||
{WIDGETS.map((widget) => (
|
||||
<div
|
||||
key={widget.type}
|
||||
draggable
|
||||
onDragStart={(e) => handleWidgetDragStart(e, widget.type)}
|
||||
onClick={() => addNewLayer(widget.type)}
|
||||
className="bg-white/5 hover:bg-white/10 border border-white/5 rounded-lg p-3 flex flex-col items-center justify-center cursor-grab active:cursor-grabbing transition-all group"
|
||||
>
|
||||
<BaseIcon path={widget.icon} size={20} className="text-gray-400 group-hover:text-blue-400 mb-2" />
|
||||
<span className="text-[10px] font-medium text-gray-400">{widget.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : sidebarTab === 'layers' ? (
|
||||
<div className="p-2 space-y-0.5">
|
||||
{pageLayers.length === 0 ? (
|
||||
<div className="p-8 text-[10px] text-gray-600 italic text-center">No layers yet</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center px-3 py-1.5 text-xs text-gray-400 font-medium">
|
||||
<BaseIcon path={mdiMonitor} size={14} className="mr-2 opacity-50" />
|
||||
<span>Body</span>
|
||||
</div>
|
||||
<div className="pl-4 space-y-0.5">
|
||||
{pageLayers.map((layer: any) => (
|
||||
<div
|
||||
key={layer.id}
|
||||
onClick={() => setSelectedLayerId(layer.id)}
|
||||
className={`flex items-center px-3 py-1.5 rounded cursor-pointer text-[12px] group transition-colors ${
|
||||
selectedLayerId === layer.id ? 'bg-blue-600 text-white' : 'hover:bg-white/5 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<BaseIcon
|
||||
path={WIDGETS.find(w => w.type === layer.layer_type)?.icon || mdiSquareOutline}
|
||||
size={14}
|
||||
className={`mr-2 ${selectedLayerId === layer.id ? 'opacity-100' : 'opacity-40'}`}
|
||||
/>
|
||||
<span className="truncate flex-grow">{layer.name || layer.layer_type}</span>
|
||||
{selectedLayerId === layer.id && (
|
||||
<BaseIcon path={mdiFlash} size={12} className="text-white/60" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center space-y-3">
|
||||
<BaseIcon path={mdiCloudUploadOutline} size={32} className="mx-auto text-gray-700" />
|
||||
<p className="text-[10px] text-gray-600">Drag and drop assets here to upload</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- MAIN CANVAS AREA --- */}
|
||||
<div
|
||||
className="flex-grow relative overflow-auto bg-[#080808] flex items-center justify-center p-60"
|
||||
onClick={() => setSelectedLayerId(null)}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleCanvasDrop}
|
||||
>
|
||||
{/* Canvas Wrapper (The Frame) */}
|
||||
<div className="flex flex-col items-center">
|
||||
{/* Frame Info Header */}
|
||||
<div className="flex items-center justify-between w-full px-2 mb-2 text-[10px] font-mono text-gray-600">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BaseIcon path={mdiVectorSquare} size={12} />
|
||||
<span>{viewportWidth === 1440 ? 'Desktop' : viewportWidth === 768 ? 'Tablet' : 'Mobile'}</span>
|
||||
</div>
|
||||
<span>{viewportWidth}px × 900px</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className="bg-white shadow-[0_0_80px_rgba(0,0,0,0.8)] relative transition-all duration-500 ease-in-out"
|
||||
style={{
|
||||
width: `${viewportWidth}px`,
|
||||
height: '900px',
|
||||
transform: `scale(${zoom / 100})`,
|
||||
transformOrigin: 'top center'
|
||||
}}
|
||||
>
|
||||
{/* Grid Pattern (Hidden in screenshot, but good for UX) */}
|
||||
{/* <div className="absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:20px_20px] pointer-events-none opacity-20"></div> */}
|
||||
|
||||
{/* Render Layers */}
|
||||
{pageLayers.map((layer: any) => {
|
||||
const isSelected = selectedLayerId === layer.id;
|
||||
const displayLayer = isSelected && localLayer ? localLayer : {
|
||||
...layer,
|
||||
parsedProps: layer.props ? JSON.parse(layer.props) : {}
|
||||
};
|
||||
const props = displayLayer.parsedProps;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={layer.id}
|
||||
onMouseDown={(e) => handleMouseDown(e, layer)}
|
||||
className={`absolute cursor-pointer flex items-center justify-center overflow-hidden transition-shadow ${
|
||||
isSelected ? 'ring-1 ring-blue-500 shadow-2xl z-50' : 'hover:ring-1 hover:ring-blue-500/30'
|
||||
}`}
|
||||
style={{
|
||||
left: Number(props.x) || 0,
|
||||
top: Number(props.y) || 0,
|
||||
width: Number(props.width) || 100,
|
||||
height: Number(props.height) || 100,
|
||||
backgroundColor: props.backgroundColor || 'transparent',
|
||||
color: props.color || '#000',
|
||||
fontSize: props.fontSize || '16px',
|
||||
borderRadius: props.borderRadius || '0px',
|
||||
zIndex: layer.order || 0,
|
||||
}}
|
||||
>
|
||||
{/* Element Label (Visible when hovering or selected) */}
|
||||
{isSelected && (
|
||||
<div className="absolute -top-5 left-0 bg-blue-500 text-white text-[9px] px-1.5 py-0.5 rounded-t-sm whitespace-nowrap pointer-events-none">
|
||||
{displayLayer.name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{layer.layer_type === 'text' && (
|
||||
<div style={{ padding: props.padding || '0px' }}>
|
||||
{displayLayer.name || 'Text Element'}
|
||||
</div>
|
||||
)}
|
||||
{layer.layer_type === 'button' && (
|
||||
<button
|
||||
className="w-full h-full font-bold pointer-events-none transition-colors"
|
||||
style={{
|
||||
backgroundColor: props.backgroundColor,
|
||||
color: props.color,
|
||||
borderRadius: props.borderRadius
|
||||
}}
|
||||
>
|
||||
{displayLayer.name || 'Button'}
|
||||
</button>
|
||||
)}
|
||||
{layer.layer_type === 'card' && (
|
||||
<CardBox className="w-full h-full pointer-events-none m-0 shadow-none border-none overflow-hidden">
|
||||
<div className="p-4 h-full flex flex-col justify-center text-center">
|
||||
<h4 className="font-bold text-lg mb-1">{displayLayer.name}</h4>
|
||||
<p className="text-xs opacity-60">Visual component integration</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
)}
|
||||
{layer.layer_type === 'calendar' && (
|
||||
<div className="w-full h-full bg-gray-50 p-4 pointer-events-none flex flex-col overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-4 border-b pb-2">
|
||||
<span className="font-bold text-gray-800 text-sm">Jan 2026</span>
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-3 h-3 rounded bg-gray-200"></div>
|
||||
<div className="w-3 h-3 rounded bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-1 flex-grow">
|
||||
{Array.from({ length: 28 }).map((_, i) => (
|
||||
<div key={i} className={`rounded border flex items-center justify-center text-[10px] ${i === 23 ? 'bg-blue-100 border-blue-300 text-blue-600 font-bold' : 'bg-white border-gray-100'}`}>
|
||||
{i + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{layer.layer_type === 'image' && (
|
||||
<div className="w-full h-full bg-gray-200 flex flex-col items-center justify-center text-gray-400 border-2 border-dashed border-gray-300">
|
||||
<BaseIcon path={mdiImageOutline} size={32} />
|
||||
</div>
|
||||
)}
|
||||
{layer.layer_type === 'frame' && !props.backgroundColor && (
|
||||
<div className="w-full h-full border-2 border-dashed border-gray-300"></div>
|
||||
)}
|
||||
|
||||
{isSelected && (
|
||||
<div className="absolute -bottom-1 -right-1 w-2.5 h-2.5 bg-white border-2 border-blue-500 rounded-full cursor-nwse-resize z-50 shadow-sm"></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- RIGHT SIDEBAR (Tabs: Style, Settings, Interactions) --- */}
|
||||
<div className="w-72 border-l border-white/5 bg-[#111111] flex flex-col">
|
||||
<div className="flex border-b border-white/5 px-2">
|
||||
{(['style', 'settings', 'interactions'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTabRight(tab)}
|
||||
className={`px-4 py-3 text-[10px] font-bold uppercase tracking-wider transition-all border-b-2 ${
|
||||
activeTabRight === tab ? 'text-white border-white' : 'text-gray-500 border-transparent hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
{!localLayer ? (
|
||||
<div className="flex flex-col items-center justify-center p-12 text-center h-full">
|
||||
<BaseIcon path={mdiCursorDefaultClickOutline} size={48} className="text-gray-800 mb-6" />
|
||||
<h3 className="text-white text-xs font-bold mb-2 uppercase tracking-widest">None Selected</h3>
|
||||
<p className="text-[10px] text-gray-600 leading-relaxed">
|
||||
Select an element on the canvas to activate this panel
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 space-y-6">
|
||||
{activeTabRight === 'style' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="text-[10px] font-bold text-gray-600 uppercase block mb-3 tracking-widest">Layout</label>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<span className="text-[9px] text-gray-500 uppercase">X</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={localLayer.parsedProps.x || 0}
|
||||
onChange={(e) => handlePropertyChange('x', Number(e.target.value))}
|
||||
onBlur={handleBlur}
|
||||
className="w-full bg-[#181818] border border-white/5 rounded px-2 py-1.5 text-[11px] outline-none focus:border-white/20 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<span className="text-[9px] text-gray-500 uppercase">Y</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={localLayer.parsedProps.y || 0}
|
||||
onChange={(e) => handlePropertyChange('y', Number(e.target.value))}
|
||||
onBlur={handleBlur}
|
||||
className="w-full bg-[#181818] border border-white/5 rounded px-2 py-1.5 text-[11px] outline-none focus:border-white/20 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<span className="text-[9px] text-gray-500 uppercase">Width</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={localLayer.parsedProps.width || 0}
|
||||
onChange={(e) => handlePropertyChange('width', Number(e.target.value))}
|
||||
onBlur={handleBlur}
|
||||
className="w-full bg-[#181818] border border-white/5 rounded px-2 py-1.5 text-[11px] outline-none focus:border-white/20 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<span className="text-[9px] text-gray-500 uppercase">Height</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={localLayer.parsedProps.height || 0}
|
||||
onChange={(e) => handlePropertyChange('height', Number(e.target.value))}
|
||||
onBlur={handleBlur}
|
||||
className="w-full bg-[#181818] border border-white/5 rounded px-2 py-1.5 text-[11px] outline-none focus:border-white/20 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-white/5">
|
||||
<label className="text-[10px] font-bold text-gray-600 uppercase block mb-3 tracking-widest">Fill & Stroke</label>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] text-gray-400">Background</span>
|
||||
<div className="flex items-center space-x-2 bg-[#181818] rounded px-2 py-1 border border-white/5">
|
||||
<input
|
||||
type="color"
|
||||
value={localLayer.parsedProps.backgroundColor || '#ffffff'}
|
||||
onChange={(e) => handlePropertyChange('backgroundColor', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className="w-4 h-4 bg-transparent border-none cursor-pointer"
|
||||
/>
|
||||
<span className="text-[10px] font-mono text-gray-500 uppercase">
|
||||
{localLayer.parsedProps.backgroundColor?.toUpperCase() || '#FFFFFF'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] text-gray-400">Text Color</span>
|
||||
<div className="flex items-center space-x-2 bg-[#181818] rounded px-2 py-1 border border-white/5">
|
||||
<input
|
||||
type="color"
|
||||
value={localLayer.parsedProps.color || '#000000'}
|
||||
onChange={(e) => handlePropertyChange('color', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className="w-4 h-4 bg-transparent border-none cursor-pointer"
|
||||
/>
|
||||
<span className="text-[10px] font-mono text-gray-500 uppercase">
|
||||
{localLayer.parsedProps.color?.toUpperCase() || '#000000'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-white/5">
|
||||
<label className="text-[10px] font-bold text-gray-600 uppercase block mb-3 tracking-widest">Radius</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localLayer.parsedProps.borderRadius || '0px'}
|
||||
onChange={(e) => handlePropertyChange('borderRadius', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className="w-full bg-[#181818] border border-white/5 rounded px-2 py-1.5 text-[11px] outline-none focus:border-white/20"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTabRight === 'settings' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-[10px] font-bold text-gray-600 uppercase block mb-2 tracking-wider">Layer Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localLayer.name || ''}
|
||||
onChange={(e) => handlePropertyChange('name', e.target.value, false)}
|
||||
onBlur={handleBlur}
|
||||
className="w-full bg-[#181818] border border-white/5 rounded px-2 py-1.5 text-xs outline-none focus:border-white/20"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-4 space-y-3">
|
||||
<button
|
||||
onClick={() => removeLayer(localLayer.id)}
|
||||
className="w-full py-2 bg-red-500/10 hover:bg-red-500/20 text-red-500 rounded text-[10px] font-bold transition-colors uppercase tracking-widest"
|
||||
>
|
||||
Delete Element
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTabRight === 'interactions' && (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<BaseIcon path={mdiFlashOutline} size={32} className="text-gray-800 mb-4" />
|
||||
<p className="text-[10px] text-gray-600">No interactions defined</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-white/5 bg-black/20 flex justify-between items-center">
|
||||
<span className="text-[10px] text-gray-600 font-mono uppercase tracking-tighter">Project v1.0.4</span>
|
||||
<BaseIcon path={mdiHelpCircleOutline} size={14} className="text-gray-700 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageBuilder;
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import BaseIcon from '../BaseIcon';
|
||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||
import { mdiEye, mdiTrashCan, mdiPencilOutline, mdiVectorSquare } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver";
|
||||
import dataFormatter from '../../helpers/dataFormatter'
|
||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||
import ListActionsPopover from '../ListActionsPopover';
|
||||
import BaseButton from '../BaseButton';
|
||||
|
||||
import {hasPermission} from "../../helpers/userPermissions";
|
||||
|
||||
@ -184,19 +185,26 @@ export const loadColumns = async (
|
||||
getActions: (params: GridRowParams) => {
|
||||
|
||||
return [
|
||||
<div key={params?.row?.id}>
|
||||
<div key={params?.row?.id} className="flex items-center">
|
||||
<BaseButton
|
||||
className="mr-2"
|
||||
href={`/pages/pages-builder/?id=${params?.row?.id}`}
|
||||
icon={mdiVectorSquare}
|
||||
color="info"
|
||||
small
|
||||
/>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={params?.row?.id}
|
||||
pathEdit={`/pages/pages-edit/?id=${params?.row?.id}`}
|
||||
pathView={`/pages/pages-view/?id=${params?.row?.id}`}
|
||||
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
|
||||
/>
|
||||
onDelete={onDelete}
|
||||
itemId={params?.row?.id}
|
||||
pathEdit={`/pages/pages-edit/?id=${params?.row?.id}`}
|
||||
pathView={`/pages/pages-view/?id=${params?.row?.id}`}
|
||||
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
|
||||
/>
|
||||
</div>,
|
||||
]
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,8 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
||||
import BaseIcon from "../components/BaseIcon";
|
||||
import { getPageTitle } from '../config'
|
||||
import Link from "next/link";
|
||||
import CardBox from '../components/CardBox';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
|
||||
import { hasPermission } from "../helpers/userPermissions";
|
||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||
@ -16,6 +18,7 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
|
||||
const Dashboard = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||
@ -24,7 +27,6 @@ const Dashboard = () => {
|
||||
|
||||
const loadingMessage = 'Loading...';
|
||||
|
||||
|
||||
const [users, setUsers] = React.useState(loadingMessage);
|
||||
const [roles, setRoles] = React.useState(loadingMessage);
|
||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
||||
@ -39,31 +41,24 @@ const Dashboard = () => {
|
||||
const [styles, setStyles] = React.useState(loadingMessage);
|
||||
const [teams, setTeams] = React.useState(loadingMessage);
|
||||
|
||||
|
||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
||||
role: { value: '', label: '' },
|
||||
});
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
||||
|
||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
||||
|
||||
|
||||
const organizationId = currentUser?.organizations?.id;
|
||||
|
||||
async function loadData() {
|
||||
const entities = ['users','roles','permissions','organizations','projects','pages','components','layers','assets','versions','builds','styles','teams',];
|
||||
const fns = [setUsers,setRoles,setPermissions,setOrganizations,setProjects,setPages,setComponents,setLayers,setAssets,setVersions,setBuilds,setStyles,setTeams,];
|
||||
const entities = ['users','roles','permissions','organizations','projects','pages','components','layers','assets','versions','builds','styles','teams'];
|
||||
const fns = [setUsers,setRoles,setPermissions,setOrganizations,setProjects,setPages,setComponents,setLayers,setAssets,setVersions,setBuilds,setStyles,setTeams];
|
||||
|
||||
const requests = entities.map((entity, index) => {
|
||||
|
||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
||||
} else {
|
||||
fns[index](null);
|
||||
return Promise.resolve({data: {count: null}});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Promise.allSettled(requests).then((results) => {
|
||||
@ -77,9 +72,10 @@ const Dashboard = () => {
|
||||
});
|
||||
}
|
||||
|
||||
async function getWidgets(roleId) {
|
||||
async function getWidgets(roleId: string) {
|
||||
await dispatch(fetchWidgets(roleId));
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
loadData().then();
|
||||
@ -94,17 +90,28 @@ const Dashboard = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{getPageTitle('Overview')}
|
||||
</title>
|
||||
<title>{getPageTitle('Overview')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={icon.mdiChartTimelineVariant}
|
||||
title='Overview'
|
||||
main>
|
||||
<SectionTitleLineWithButton icon={icon.mdiChartTimelineVariant} title='Overview' main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
{/* Welcome Banner */}
|
||||
<CardBox className="mb-6 bg-gradient-to-r from-blue-600 to-indigo-700 text-white border-none shadow-xl">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between p-4">
|
||||
<div className="space-y-2 mb-4 md:mb-0">
|
||||
<h2 className="text-2xl font-bold">Welcome back, {currentUser?.firstName || 'Builder'}!</h2>
|
||||
<p className="text-blue-100 opacity-80">Ready to design your next masterpiece? Jump back into your projects.</p>
|
||||
</div>
|
||||
<BaseButton
|
||||
href="/projects/projects-new"
|
||||
label="Create New Project"
|
||||
color="white"
|
||||
className="font-bold text-blue-600 rounded-full px-6"
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||
currentUser={currentUser}
|
||||
@ -112,409 +119,76 @@ const Dashboard = () => {
|
||||
setWidgetsRole={setWidgetsRole}
|
||||
widgetsRole={widgetsRole}
|
||||
/>}
|
||||
{!!rolesWidgets.length &&
|
||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!!rolesWidgets.length && hasPermission(currentUser, 'CREATE_ROLES') && (
|
||||
<p className='text-gray-500 dark:text-gray-400 mb-4'>
|
||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
||||
{(isFetchingQuery || loading) && (
|
||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||
<BaseIcon
|
||||
className={`${iconsColor} animate-spin mr-5`}
|
||||
w='w-16'
|
||||
h='h-16'
|
||||
size={48}
|
||||
path={icon.mdiLoading}
|
||||
/>{' '}
|
||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||
<BaseIcon className={`${iconsColor} animate-spin mr-5`} w='w-16' h='h-16' size={48} path={icon.mdiLoading} />{' '}
|
||||
Loading widgets...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ rolesWidgets &&
|
||||
rolesWidgets.map((widget) => (
|
||||
<SmartWidget
|
||||
key={widget.id}
|
||||
userId={currentUser?.id}
|
||||
widget={widget}
|
||||
roleId={widgetsRole?.role?.value || ''}
|
||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
||||
/>
|
||||
{rolesWidgets && rolesWidgets.map((widget) => (
|
||||
<SmartWidget
|
||||
key={widget.id}
|
||||
userId={currentUser?.id}
|
||||
widget={widget}
|
||||
roleId={widgetsRole?.role?.value || ''}
|
||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
||||
{!!rolesWidgets.length && <hr className='my-6' />}
|
||||
|
||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
||||
|
||||
|
||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Users
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{users}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Roles
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{roles}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Permissions
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{permissions}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_ORGANIZATIONS') && <Link href={'/organizations/organizations-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Organizations
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{organizations}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_PROJECTS') && <Link href={'/projects/projects-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 hover:shadow-lg transition-shadow cursor-pointer`}>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Projects
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{projects}
|
||||
</div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Projects</div>
|
||||
<div className="text-3xl leading-tight font-semibold">{projects}</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiFolder' in icon ? icon['mdiFolder' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
<BaseIcon className={`${iconsColor}`} w="w-16" h="h-16" size={48} path={icon.mdiFolder} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
|
||||
{hasPermission(currentUser, 'READ_PAGES') && <Link href={'/pages/pages-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 hover:shadow-lg transition-shadow cursor-pointer`}>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Pages
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{pages}
|
||||
</div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Pages</div>
|
||||
<div className="text-3xl leading-tight font-semibold">{pages}</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiWeb' in icon ? icon['mdiWeb' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
<BaseIcon className={`${iconsColor}`} w="w-16" h="h-16" size={48} path={icon.mdiWeb} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
|
||||
{hasPermission(currentUser, 'READ_COMPONENTS') && <Link href={'/components/components-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 hover:shadow-lg transition-shadow cursor-pointer`}>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Components
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{components}
|
||||
</div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Components</div>
|
||||
<div className="text-3xl leading-tight font-semibold">{components}</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiPuzzle' in icon ? icon['mdiPuzzle' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
<BaseIcon className={`${iconsColor}`} w="w-16" h="h-16" size={48} path={icon.mdiPuzzle} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_LAYERS') && <Link href={'/layers/layers-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Layers
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{layers}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiLayers' in icon ? icon['mdiLayers' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_ASSETS') && <Link href={'/assets/assets-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Assets
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{assets}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiFolderImage' in icon ? icon['mdiFolderImage' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_VERSIONS') && <Link href={'/versions/versions-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Versions
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{versions}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiHistory' in icon ? icon['mdiHistory' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_BUILDS') && <Link href={'/builds/builds-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Builds
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{builds}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCloudUpload' in icon ? icon['mdiCloudUpload' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_STYLES') && <Link href={'/styles/styles-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Styles
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{styles}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiFormatPaint' in icon ? icon['mdiFormatPaint' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_TEAMS') && <Link href={'/teams/teams-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Teams
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{teams}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
|
||||
</div>
|
||||
</SectionMain>
|
||||
</>
|
||||
@ -525,4 +199,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
export default Dashboard
|
||||
@ -1,166 +1,145 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('image');
|
||||
const [contentPosition, setContentPosition] = useState('background');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
|
||||
const title = 'Framer-like Page Builder'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="bg-black text-white min-h-screen font-sans selection:bg-blue-500 selection:text-white">
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Framer-like Page Builder')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your Framer-like Page Builder app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
{/* Navigation */}
|
||||
<nav className="flex items-center justify-between px-6 py-6 md:px-12 border-b border-white/10 sticky top-0 bg-black/50 backdrop-blur-xl z-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-tr from-blue-600 to-purple-600 rounded-lg flex items-center justify-center font-bold text-xl shadow-lg shadow-blue-500/20">
|
||||
F
|
||||
</div>
|
||||
<span className="text-xl font-bold tracking-tight">FramerClone</span>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center space-x-8 text-sm font-medium text-gray-400">
|
||||
<a href="#features" className="hover:text-white transition-colors">Features</a>
|
||||
<a href="#showcase" className="hover:text-white transition-colors">Showcase</a>
|
||||
<a href="#pricing" className="hover:text-white transition-colors">Pricing</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/login" className="text-sm font-medium text-gray-400 hover:text-white transition-colors">
|
||||
Login
|
||||
</Link>
|
||||
<BaseButton
|
||||
href="/register"
|
||||
label="Get Started"
|
||||
color="info"
|
||||
className="rounded-full px-6 py-2 text-sm font-bold bg-blue-600 hover:bg-blue-500 border-none"
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<SectionFullScreen>
|
||||
<div className="max-w-6xl mx-auto px-6 pt-20 pb-32 text-center relative overflow-hidden">
|
||||
{/* Background Glow */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-blue-600/10 rounded-full blur-[120px] -z-10 animate-pulse"></div>
|
||||
|
||||
<h1 className="text-5xl md:text-8xl font-black tracking-tighter mb-8 leading-[0.9] animate-fade-in-up">
|
||||
DESIGN & PUBLISH <br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
|
||||
IN SECONDS
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-gray-400 max-w-2xl mx-auto mb-12 font-medium leading-relaxed">
|
||||
The professional page builder for designers. Drag, drop, and ship pixel-perfect websites without writing code.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center justify-center space-y-4 md:space-y-0 md:space-x-6">
|
||||
<BaseButton
|
||||
href="/register"
|
||||
label="Start Building Free"
|
||||
color="info"
|
||||
className="w-full md:w-auto rounded-full px-10 py-4 text-lg font-bold bg-blue-600 hover:bg-blue-500 border-none shadow-2xl shadow-blue-500/40"
|
||||
/>
|
||||
<Link href="/login" className="text-gray-400 hover:text-white font-medium flex items-center group transition-colors">
|
||||
Watch Demo
|
||||
<svg className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Canvas Preview Mockup */}
|
||||
<div className="mt-24 relative group">
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-blue-600 to-purple-600 rounded-2xl blur opacity-25 group-hover:opacity-40 transition duration-1000"></div>
|
||||
<div className="relative bg-zinc-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
|
||||
<div className="flex items-center px-4 py-2 border-b border-white/5 bg-zinc-950/50">
|
||||
<div className="flex space-x-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500/50"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500/50"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-green-500/50"></div>
|
||||
</div>
|
||||
<div className="mx-auto text-[10px] text-gray-500 font-mono tracking-widest uppercase">page-editor.framerclone.io</div>
|
||||
</div>
|
||||
<div className="h-[400px] md:h-[600px] bg-black flex relative overflow-hidden">
|
||||
{/* Editor UI Mockup */}
|
||||
<div className="w-64 border-r border-white/5 bg-zinc-950 hidden md:block p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="h-4 w-2/3 bg-white/5 rounded"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-8 bg-blue-600/10 border border-blue-500/20 rounded-md flex items-center px-3">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-sm mr-2"></div>
|
||||
<div className="h-3 w-1/2 bg-blue-500/50 rounded"></div>
|
||||
</div>
|
||||
<div className="h-8 bg-white/5 rounded-md"></div>
|
||||
<div className="h-8 bg-white/5 rounded-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow bg-zinc-900/50 relative overflow-hidden cursor-crosshair">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-3/4 h-3/4 bg-black border border-white/10 rounded-xl shadow-2xl flex flex-col items-center justify-center space-y-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-tr from-blue-500 to-purple-500 rounded-full animate-bounce"></div>
|
||||
<div className="h-4 w-48 bg-white/10 rounded-full"></div>
|
||||
<div className="h-4 w-32 bg-white/5 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-72 border-l border-white/5 bg-zinc-950 hidden md:block p-4">
|
||||
<div className="space-y-6">
|
||||
<div className="h-4 w-1/3 bg-white/10 rounded"></div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="h-10 bg-white/5 rounded"></div>
|
||||
<div className="h-10 bg-white/5 rounded"></div>
|
||||
</div>
|
||||
<div className="h-40 bg-white/5 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-white/10 py-12 px-6">
|
||||
<div className="max-w-6xl mx-auto flex flex-col md:flex-row justify-between items-center space-y-6 md:space-y-0">
|
||||
<div className="flex items-center space-x-2 opacity-50">
|
||||
<div className="w-6 h-6 bg-white rounded flex items-center justify-center font-bold text-black text-xs">F</div>
|
||||
<span className="font-bold tracking-tight">FramerClone</span>
|
||||
</div>
|
||||
<div className="flex space-x-8 text-sm text-gray-500">
|
||||
<Link href="/terms-of-use" className="hover:text-white transition-colors">Terms</Link>
|
||||
<Link href="/privacy-policy" className="hover:text-white transition-colors">Privacy</Link>
|
||||
<span className="">© 2026 FramerClone</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
Home.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
||||
import { mdiChartTimelineVariant, mdiVectorSquare } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import DatePicker from "react-datepicker";
|
||||
@ -16,316 +16,45 @@ import FormField from '../../components/FormField'
|
||||
import BaseDivider from '../../components/BaseDivider'
|
||||
import BaseButtons from '../../components/BaseButtons'
|
||||
import BaseButton from '../../components/BaseButton'
|
||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
||||
import FormFilePicker from '../../components/FormFilePicker'
|
||||
import FormImagePicker from '../../components/FormImagePicker'
|
||||
import { SelectField } from "../../components/SelectField";
|
||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||
import { SwitchField } from '../../components/SwitchField'
|
||||
import {RichTextField} from "../../components/RichTextField";
|
||||
|
||||
import { update, fetch } from '../../stores/pages/pagesSlice'
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import {saveFile} from "../../helpers/fileSaver";
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import ImageField from "../../components/ImageField";
|
||||
|
||||
import {hasPermission} from "../../helpers/userPermissions";
|
||||
|
||||
|
||||
|
||||
const EditPages = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const initVals = {
|
||||
|
||||
|
||||
'title': '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'slug': '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'path': '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
project: null,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
published: false,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
publish_start: new Date(),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
publish_end: new Date(),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'seo_description': '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
organizations: null,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
const [initialValues, setInitialValues] = useState(initVals)
|
||||
|
||||
const { pages } = useAppSelector((state) => state.pages)
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
|
||||
const { pagesId } = router.query
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id: pagesId }))
|
||||
}, [pagesId])
|
||||
if (pagesId) {
|
||||
dispatch(fetch({ id: pagesId }))
|
||||
}
|
||||
}, [pagesId, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof pages === 'object') {
|
||||
setInitialValues(pages)
|
||||
if (typeof pages === 'object' && pages !== null) {
|
||||
setInitialValues(pages as any)
|
||||
}
|
||||
}, [pages])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof pages === 'object') {
|
||||
|
||||
const newInitialVal = {...initVals};
|
||||
|
||||
Object.keys(initVals).forEach(el => newInitialVal[el] = (pages)[el])
|
||||
|
||||
setInitialValues(newInitialVal);
|
||||
}
|
||||
}, [pages])
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
const handleSubmit = async (data: any) => {
|
||||
await dispatch(update({ id: pagesId, data }))
|
||||
await router.push('/pages/pages-list')
|
||||
}
|
||||
@ -337,7 +66,13 @@ const EditPages = () => {
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit pages'} main>
|
||||
{''}
|
||||
<BaseButton
|
||||
href={`/pages/pages-builder/?id=${pagesId}`}
|
||||
icon={mdiVectorSquare}
|
||||
label="Open Builder"
|
||||
color="info"
|
||||
rounded-full
|
||||
/>
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
@ -346,424 +81,85 @@ const EditPages = () => {
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Title"
|
||||
>
|
||||
<Field
|
||||
name="title"
|
||||
placeholder="Title"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Slug"
|
||||
>
|
||||
<Field
|
||||
name="slug"
|
||||
placeholder="Slug"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Path"
|
||||
>
|
||||
<Field
|
||||
name="path"
|
||||
placeholder="Path"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Project' labelFor='project'>
|
||||
<Field
|
||||
name='project'
|
||||
id='project'
|
||||
component={SelectField}
|
||||
options={initialValues.project}
|
||||
itemRef={'projects'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
showField={'name'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Published' labelFor='published'>
|
||||
<Field
|
||||
name='published'
|
||||
id='published'
|
||||
component={SwitchField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Publishstart"
|
||||
>
|
||||
<DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={initialValues.publish_start ?
|
||||
new Date(
|
||||
dayjs(initialValues.publish_start).format('YYYY-MM-DD hh:mm'),
|
||||
) : null
|
||||
}
|
||||
onChange={(date) => setInitialValues({...initialValues, 'publish_start': date})}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Publishend"
|
||||
>
|
||||
<DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={initialValues.publish_end ?
|
||||
new Date(
|
||||
dayjs(initialValues.publish_end).format('YYYY-MM-DD hh:mm'),
|
||||
) : null
|
||||
}
|
||||
onChange={(date) => setInitialValues({...initialValues, 'publish_end': date})}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="SEOdescription"
|
||||
>
|
||||
<Field
|
||||
name="seo_description"
|
||||
placeholder="SEOdescription"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='organizations' labelFor='organizations'>
|
||||
<Field
|
||||
name='organizations'
|
||||
id='organizations'
|
||||
component={SelectField}
|
||||
options={initialValues.organizations}
|
||||
itemRef={'organizations'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
showField={'name'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Title">
|
||||
<Field name="title" placeholder="Title" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Slug">
|
||||
<Field name="slug" placeholder="Slug" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Path">
|
||||
<Field name="path" placeholder="Path" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Project" labelFor="project">
|
||||
<Field
|
||||
name="project"
|
||||
id="project"
|
||||
component={SelectField}
|
||||
options={initialValues.project}
|
||||
itemRef={'projects'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Published" labelFor="published">
|
||||
<Field name="published" id="published" component={SwitchField}></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Publishstart">
|
||||
<DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={
|
||||
initialValues.publish_start
|
||||
? new Date(dayjs(initialValues.publish_start).format('YYYY-MM-DD hh:mm'))
|
||||
: null
|
||||
}
|
||||
onChange={(date) => setInitialValues({ ...initialValues, publish_start: date })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Publishend">
|
||||
<DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={
|
||||
initialValues.publish_end
|
||||
? new Date(dayjs(initialValues.publish_end).format('YYYY-MM-DD hh:mm'))
|
||||
: null
|
||||
}
|
||||
onChange={(date) => setInitialValues({ ...initialValues, publish_end: date })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="SEOdescription">
|
||||
<Field name="seo_description" placeholder="SEOdescription" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="organizations" labelFor="organizations">
|
||||
<Field
|
||||
name="organizations"
|
||||
id="organizations"
|
||||
component={SelectField}
|
||||
options={initialValues.organizations}
|
||||
itemRef={'organizations'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/pages/pages-list')}/>
|
||||
<BaseButton
|
||||
type="reset"
|
||||
color="danger"
|
||||
outline
|
||||
label="Cancel"
|
||||
onClick={() => router.push('/pages/pages-list')}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
@ -774,15 +170,7 @@ const EditPages = () => {
|
||||
}
|
||||
|
||||
EditPages.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'UPDATE_PAGES'}
|
||||
|
||||
>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
return <LayoutAuthenticated permission={'UPDATE_PAGES'}>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default EditPages
|
||||
export default EditPages
|
||||
32
frontend/src/pages/pages/pages-builder.tsx
Normal file
32
frontend/src/pages/pages/pages-builder.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import PageBuilder from '../../components/Pages/PageBuilder';
|
||||
import { getPageTitle } from '../../config';
|
||||
|
||||
const BuilderPage = () => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Page Builder')}</title>
|
||||
</Head>
|
||||
{id && <PageBuilder pageId={id as string} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BuilderPage.getLayout = function getLayout(page: ReactElement) {
|
||||
// We don't use the standard Authenticated layout because the builder
|
||||
// needs a custom full-screen UI without the standard sidebar/navbar.
|
||||
// However, we still want auth. A better approach is a dedicated
|
||||
// "BuilderLayout" or just checking auth inside.
|
||||
// For now, let's keep it simple and ensure the user is logged in
|
||||
// if needed by checking in the component.
|
||||
return page;
|
||||
};
|
||||
|
||||
export default BuilderPage;
|
||||
@ -1,4 +1,4 @@
|
||||
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
||||
import { mdiChartTimelineVariant, mdiUpload, mdiVectorSquare } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import DatePicker from "react-datepicker";
|
||||
@ -334,7 +334,7 @@ const EditPagesPage = () => {
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit pages'} main>
|
||||
{''}
|
||||
<BaseButton href={`/pages/pages-builder/?id=${id}`} icon={mdiVectorSquare} label="Open Builder" color="info" rounded-full />
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
@ -782,4 +782,4 @@ EditPagesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default EditPagesPage
|
||||
export default EditPagesPage
|
||||
@ -16,7 +16,7 @@ import SectionMain from "../../components/SectionMain";
|
||||
import CardBox from "../../components/CardBox";
|
||||
import BaseButton from "../../components/BaseButton";
|
||||
import BaseDivider from "../../components/BaseDivider";
|
||||
import {mdiChartTimelineVariant} from "@mdi/js";
|
||||
import {mdiChartTimelineVariant, mdiVectorSquare} from "@mdi/js";
|
||||
import {SwitchField} from "../../components/SwitchField";
|
||||
import FormField from "../../components/FormField";
|
||||
|
||||
@ -50,11 +50,21 @@ const PagesView = () => {
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View pages')} main>
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Edit'
|
||||
href={`/pages/pages-edit/?id=${id}`}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<BaseButton
|
||||
href={`/pages/pages-builder/?id=${id}`}
|
||||
icon={mdiVectorSquare}
|
||||
label="Open Builder"
|
||||
color="info"
|
||||
rounded-full
|
||||
className="mr-2"
|
||||
/>
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Edit'
|
||||
href={`/pages/pages-edit/?id=${id}`}
|
||||
/>
|
||||
</div>
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
|
||||
@ -462,7 +472,7 @@ const PagesView = () => {
|
||||
|
||||
|
||||
|
||||
<th>Order</th>
|
||||
<th>Order</th>
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { useAppDispatch } from '../stores/hooks';
|
||||
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||
@ -15,7 +13,6 @@ import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
|
||||
const SearchView = () => {
|
||||
@ -30,22 +27,20 @@ const SearchView = () => {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchData());
|
||||
}, [dispatch, searchQuery]);
|
||||
|
||||
const fetchData = createAsyncThunk('/search', async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post('/search', { searchQuery , organizationsId});
|
||||
setSearchResults(response.data);
|
||||
setLoading(false);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error.response);
|
||||
setLoading(false);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
const fetchData = async () => {
|
||||
if (!searchQuery) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post('/search', { searchQuery , organizationsId});
|
||||
setSearchResults(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [searchQuery, organizationsId]);
|
||||
|
||||
const groupedResults = searchResults.reduce((acc, item) => {
|
||||
const { tableName } = item;
|
||||
@ -93,4 +88,4 @@ SearchView.getLayout = function getLayout(page: ReactElement) {
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchView;
|
||||
export default SearchView;
|
||||
Loading…
x
Reference in New Issue
Block a user