Created
January 9, 2026 18:07
-
-
Save bharatsewani1993/040aaf2d03900bc1ed9f328298cfcfc7 to your computer and use it in GitHub Desktop.
This is code for NCV PAGE BUILDER SCREEN.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use client' | |
| import React, { useEffect, useState,useCallback,useMemo, useRef,useLayoutEffect, } from "react"; | |
| import styles from '../css/website-builder.module.css' | |
| import {DesktopIcon, MonitorIcon,ClockCounterClockwiseIcon,CheckCircleIcon, CopyIcon, DatabaseIcon, Devices, EnvelopeIcon, EyeIcon, FloppyDiskBackIcon, Gear, Globe, House, Lightning, LightningIcon, MagnifyingGlassIcon, Palette, Plus, Share, Shield, ShoppingCartIcon, Sliders, Table, Trash,UserSoundIcon,X,Layout, Cards, TableIcon, PencilIcon, GlobeIcon, PaletteIcon, ShieldIcon, ShareIcon, DevicesIcon, PlusIcon, SlidersIcon, XIcon, LayoutIcon,ArrowsClockwiseIcon, TrashIcon, ArrowLeftIcon, XCircleIcon, WarningCircleIcon, HouseIcon, TrashSimpleIcon, WarningIcon, HouseSimpleIcon, ArchiveIcon, GearIcon, XLogoIcon, CircleIcon, DotsSixIcon, FilePlusIcon, CaretUpIcon, CaretDownIcon, FloppyDiskIcon, GearSixIcon, FilesIcon, PlusCircleIcon, EyesIcon, TextAaIcon, TextTIcon,MinusIcon, CaretUpDownIcon, TextBIcon, TextItalicIcon, TextUnderlineIcon, TextStrikethroughIcon, TextSuperscriptIcon, TextSubscriptIcon, TextAlignJustifyIcon, LinkIcon, InfoIcon, CheckIcon, SwapIcon, MagicWandIcon, VideoIcon, ArrowCounterClockwiseIcon, DropHalfBottomIcon, SquaresFourIcon, LinkBreakIcon, HeartIcon, ArrowsVerticalIcon, ArrowsHorizontalIcon, ArrowUpIcon, ArrowRightIcon, ArrowDownIcon,ResizeIcon, CircleNotchIcon, ChecksIcon, AlignLeftIcon, AlignRightIcon, AlignCenterVerticalIcon, FrameCornersIcon, LinkSimpleIcon, FunnelIcon, PencilLineIcon, CursorClickIcon, CursorIcon} from "@phosphor-icons/react"; | |
| import { useRouter } from "next/navigation"; | |
| import { v4 as uuidv4 } from 'uuid'; | |
| import {useRoleStore} from "../store/roleStore"; | |
| import '../css/global.css'; | |
| import '../css/fonts-all.css' | |
| import { debounce, drop, head } from "lodash"; | |
| import { jwtDecode } from "jwt-decode"; | |
| import eventBus from "../utils/eventBus"; | |
| import isEqual from "lodash/isEqual"; | |
| import PageComponentWrapper from './MemorizedPageComponent' | |
| import { iframeRootRef } from '../lib/iframeRootRef' | |
| import { usePathname } from "next/navigation"; | |
| import { Icon } from '@iconify/react'; | |
| import axios from "axios"; | |
| import { useScreenSizeCheck } from "./useScreenSizeCheck"; | |
| import { SpinnerIcon } from "@phosphor-icons/react/dist/ssr"; | |
| import { ArrowLeftRightIcon, CloudUploadIcon, SettingsIcon, UploadIcon } from "lucide-react"; | |
| import { ICON_LIBRARIES, POPULAR_ICONS, getIconDisplayName, getLibraryColor,getLibraryName,SEARCH_SYNONYMS,ICON_CATEGORIES } from "../lib/iconsConfig"; | |
| import * as PhosphorIcons from "phosphor-react"; | |
| import * as LucideIcons from "lucide-react"; | |
| import * as TablerIcons from "@tabler/icons-react"; | |
| const WebsiteBuilder=()=>{ | |
| const [activeTab,setActivetab]=useState(null); | |
| const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); | |
| const [visualActivetab,setVisualActiveTab]=useState('Pages'); | |
| const manualEditedLinksRef = useRef({}); | |
| const componentsPageRefState = useRef([]); | |
| const [toggle,setToggle]=useState(false) | |
| const [forceRender, setForceRender] = useState(0); | |
| const [sliders,setSliders]=useState(''); | |
| const [websiteId,setWebsiteId]=useState(null); | |
| const [componentsData, setComponentsData] = useState([]); | |
| const prevIframeSrcMapRef = useRef({}); | |
| const [selectedLinkId, setSelectedLinkId] = useState(null); | |
| const [WebsiteModal,setWebsiteModal]=useState(false) | |
| const [saveText,setSaveText]=useState(false); | |
| const [publishModalError,setPublishModalError]=useState(false); | |
| const [loadingComponents, setLoadingComponents] = useState(false); | |
| const [previewButton,setPreviewButton]=useState(false); | |
| const [resetIcon,setResetIcon]=useState(false); | |
| const [publishModal,setPublishModal]=useState(false); | |
| const [subdomain,setSubdomain]=useState(''); | |
| const[componentsPage,setComponentsPage]=useState([]); | |
| const [componentToggles,setComponentToggles]=useState(false) | |
| const [slidertoggle,setSliderToggle]=useState(false); | |
| const [draggedItemIndex,setDraggedItemIndex]=useState(null); | |
| const [drggingId,setDraggingId]=useState(null) | |
| const [movingPageId, setMovingPageId] = useState(null); | |
| const [draggingPageId,setDraggingPageid]=useState(null); | |
| const persistentWSRef = { current: null }; | |
| const heartbeatRef = { current: null }; | |
| const [showModel,setShowModel]=useState(false); | |
| const justAddedComponentRef=useRef(null); | |
| const [movingId, setMovingId] = useState(null); | |
| const [editingLinkkeys,setEditingLinkKeys]=useState(new Set()) | |
| const [totalAddComponent,setTotalAddComponent]=useState(0); | |
| const [selectedComponents,setSelectedComponents]=useState([]); | |
| const router=useRouter(); | |
| const [publishButton,setPublishButton]=useState(false) | |
| const [selectedComponentUrl, setSelectedComponentUrl] = useState(null); | |
| const [pages,setPages]=useState([]); | |
| const [propertiesIcon,setPropertiesIcon]=useState(false); | |
| const [fetchedPages, setFetchedPages] = useState(null); // <-- null initially | |
| const [activePage,setActivePage]=useState(''); | |
| const pageIframeRefs = useRef({}); | |
| const [loadingStates,setLoadingStates]=useState({}); | |
| const [loadingStatesPage,setLoadingStatesPage]=useState({}) | |
| const [componentBy,setComponentBy]=useState({}) | |
| const [schemas,setSchemas]=useState([]) | |
| const [pageDeleteModal,setpageDeleteModal]=useState(false); | |
| const [pageId,setPageId]=useState(null); | |
| const [showPageModel,setShowPageModel]=useState(false); | |
| const [databaseModal,setDatabaseModal]=useState(false); | |
| const [toastModel,setTotastModal]=useState(false); | |
| const iframeMap = useRef(new Map()); | |
| const [overrides,setOverides]=useState({}); | |
| const sortedComponentsPageRef = useRef([]); | |
| const componentsPageRef=useRef([]); | |
| const orderMapRef = useRef({}); | |
| const [linkInputCache,setLinkInputCache]=useState({}); | |
| const [databaseToastModal,setDatabaseToastModal]=useState(false); | |
| const [deleteDatabaseToast,setdeleteDatabaseToast]=useState(false); | |
| const [deletePageToast,setDeletePageToast]=useState(false); | |
| const [tableName,setTableName]=useState(''); | |
| const [activePageId, setActivePageId] = useState(null); | |
| const [components,setComponents]=useState([])//Array of id,url | |
| const [positionForNewComponent, setPositionForNewComponent] = useState(null); //null,topId,BottomId | |
| const [schemaId,setSchemaId]=useState(null) | |
| const [pageName,setPageName]=useState(''); | |
| const [pageComponent,setPageComponent]=useState(''); | |
| const [searchTerm, setSearchTerm] = useState(""); | |
| const [selectedPageComponent,setSelectedPageComponents]=useState([]); | |
| const iframeRefs=useRef({}); | |
| const [topPostion,setTopPostion]=useState(''); | |
| const [bottomPosition,setBottomPosition]=useState(''); | |
| const selectedComponentRefPage = useRef([]); | |
| const orderMapPageRef = useRef({}); | |
| const pathname=usePathname(); | |
| const [forceRenderPage, setForceRenderPage] = useState(0); | |
| const wsRef = useRef(null); | |
| let persistentWS=null; | |
| const socketRef=useRef(null); | |
| const [componentInstanceId,setComponentInstanceId]=useState(''); | |
| const overridesMapRef=useRef({}); | |
| const selectedComponentRef=useRef([]); | |
| const selectedPageComponentRef=useRef([]); | |
| const heightCache=useRef({}); | |
| const componentObjectMapRef = useRef({}); | |
| const debounceTimeouts = useRef({}); | |
| const messageListenerRef = useRef(null); | |
| const sentNavLinksRef = useRef({}); | |
| const [selectedComponentInstanceId,setSelectedComponentInstanceId]=useState(null); | |
| const componentsRef=useRef([]); | |
| const [manuallyEditedLinks,setManuallyEditedLinks]=useState({}); | |
| const iframeSrcMap = useRef({}); | |
| const panelRef = useRef(null); | |
| const [ActiveSection, setActiveSection] = useState(null); | |
| const panelref=useRef(null); | |
| const [activeAccordion,setActiveAccordion]=useState(null); | |
| const [activeVideoTab,setActiveVideoTab]=useState('url'); | |
| const [selectedElement, setSelectedElement] = useState(null); | |
| const [fontSize, setFontSize] = useState(16); | |
| const [lineHeightValue, setLineHeightValue] = useState(1.5); | |
| const [letterSpacingValue, setLetterSpacingValue] = useState(0); | |
| const fileInputRef = useRef(null); | |
| const [altText, setAltText] = useState(""); | |
| const [imageTitle, setImageTitle] = useState(""); | |
| const [uploading, setUploading] = useState(false); | |
| const [progress, setProgress] = useState(0); | |
| const [imageOpacity, setImageOpacity] = useState(100); | |
| const [imageBlur, setImageBlur] = useState(0); | |
| const videoFileInputRef = useRef(null); | |
| const [videoUrl, setVideoUrl] = useState(""); | |
| const [appliedUrl, setAppliedUrl] = useState(""); | |
| const [fontFamily, setFontFamily] = useState("Poppins, sans-serif"); | |
| const [textColor, setTextColor] = useState("rgb(0,0,0)"); | |
| const [lineHeight, setLineHeight] = useState("normal"); | |
| const [letterSpacing, setLetterSpacing] = useState("0px"); | |
| const [fontWeight, setFontWeight] = useState("400"); | |
| const [fontStyle, setFontStyle] = useState("normal"); | |
| const [textDecoration, setTextDecoration] = useState("none"); | |
| const [textContent, setTextContent] = useState(""); | |
| // 🎥 Video properties | |
| const [videoAutoplay, setVideoAutoplay] = useState(false); | |
| const [videoMuted, setVideoMuted] = useState(false); | |
| const [videoLoop, setVideoLoop] = useState(false); | |
| const [videoControls, setVideoControls] = useState(true); | |
| const [loading, setLoading] = useState(false); | |
| const [selectedIcon, setSelectedIcon] = useState({ | |
| name: "heart", | |
| library: "", // leave empty so font-family detection works | |
| }); | |
| // ---------------- Form States ---------------- | |
| const [formId, setFormId] = useState(""); // Form unique ID | |
| const [formTitle, setFormTitle] = useState(""); // Form title | |
| const [formDescription, setFormDescription] = useState(""); // Form description | |
| const [formBackground, setFormBackground] = useState(""); // Background image URL | |
| const [formBackgroundColor, setFormBackgroundColor] = useState("#ffffff"); // Background color | |
| const [formPadding, setFormPadding] = useState("20"); // Padding in px | |
| const [formBorderRadius, setFormBorderRadius] = useState("8"); // Border radius in px | |
| const [formMaxWidth, setFormMaxWidth] = useState("600"); // Max width in px | |
| const [formAlignment, setFormAlignment] = useState("left"); // Alignment: left/center/right | |
| const [formBorderWidth, setFormBorderWidth] = useState("2"); // Border width in px | |
| const [formBorderColor, setFormBorderColor] = useState("#E5E7EB"); // Border color | |
| const [formShadow, setFormShadow] = useState("1"); // Shadow intensity 0-3 | |
| const [formBorderStyle, setFormBorderStyle] = useState("solid"); | |
| const [dirtyFormMaxWidth, setDirtyFormMaxWidth] = useState(false); | |
| const [formBackgroundDisplay, setFormBackgroundDisplay] = useState("#ffffff"); | |
| const [selectedIconMeta, setSelectedIconMeta] = useState({ | |
| color: "#000000", | |
| fontSize: "48px", | |
| rotation: 0, | |
| flipHorizontal: false, | |
| flipVertical: false, | |
| fontFamily: "inherit", | |
| }); | |
| const [currentColor, setCurrentColor] = useState(""); | |
| const [iconColorInput, setIconColorInput] = useState("#000000"); | |
| const [currentSize, setCurrentSize] = useState(48); | |
| const [currentRotation, setCurrentRotation] = useState(0); | |
| const [isFlippedHorizontal, setIsFlippedHorizontal] = useState(false); | |
| const [isFlippedVertical, setIsFlippedVertical] = useState(false); | |
| const [search, setSearch] = useState(""); | |
| const [icons, setIcons] = useState(POPULAR_ICONS); | |
| const [bgImageSrc, setBgImageSrc] = useState(""); // background image URL | |
| const [bgImageOpacity, setBgImageOpacity] = useState(100); // default 100% | |
| const [bgImageBlur, setBgImageBlur] = useState(0); // default 0px | |
| const [uploadingBg, setUploadingBg] = useState(false); | |
| const fileInputBgRef = useRef(null); | |
| const [selectedLibrary, setSelectedLibrary] = useState("popular"); | |
| const [selectedCategory, setSelectedCategory] = useState("all"); | |
| const [activeFormats, setActiveFormats] = useState({ | |
| bold: false, | |
| italic: false, | |
| underline: false, | |
| strike: false, | |
| superscript: false, | |
| subscript: false, | |
| }); | |
| const [propertyValues, setPropertyValues] = useState({ | |
| fontFamily: "", | |
| color: "#000000", | |
| fontSize: "", | |
| backgroundColor: "#ffff00", | |
| link: "", | |
| }); | |
| const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); | |
| const [formStyleState, setFormStyleState] = useState({}); | |
| const [windowReady, setWindowReady] = useState(false); | |
| // ✅ Always top-level | |
| const manualContinueRef = useRef( | |
| typeof window !== "undefined" ? sessionStorage.getItem("manual-continue") === "true" : false | |
| ); | |
| const { isTooSmall } = useScreenSizeCheck(manualContinueRef); | |
| useEffect(() => { | |
| const updateSize = () => setDimensions({ width: window.innerWidth, height: window.innerHeight }); | |
| updateSize(); | |
| window.addEventListener("resize", updateSize); | |
| return () => window.removeEventListener("resize", updateSize); | |
| }, []); | |
| const handleContinue = () => { | |
| manualContinueRef.current = true; | |
| sessionStorage.setItem("manual-continue", "true"); | |
| }; | |
| // Debounced function to set debouncedSearchTerm | |
| const debouncedSetSearchTerm = useCallback( | |
| debounce((value) => { | |
| setDebouncedSearchTerm(value); | |
| }, 500), | |
| [] | |
| ); | |
| async function loadLibrary(dataFile, libraryId) { | |
| try { | |
| setLoading(true); | |
| const response = await fetch(`/${dataFile}`); | |
| if (!response.ok) throw new Error("Failed to load icons"); | |
| const data = await response.json(); | |
| // Make sure icons is an array | |
| const iconsArray = Array.isArray(data.icons) ? data.icons : data; | |
| // Attach library if not already present | |
| const preparedIcons = iconsArray.map(icon => ({ | |
| ...icon, | |
| library: icon.library || libraryId, | |
| })); | |
| setIcons(preparedIcons); | |
| } catch (err) { | |
| console.error("Error loading library:", err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| const isExternalLink = (href = "") => { | |
| if (!href) return false; | |
| return ( | |
| href.startsWith("http://") || | |
| href.startsWith("https://") || | |
| href.startsWith("//") | |
| ); | |
| }; | |
| const getCleanLinkLabel = (text, fallback = "Untitled link") => { | |
| if (!text) return fallback; | |
| // If array → join | |
| if (Array.isArray(text)) { | |
| return text.join(" ").trim() || fallback; | |
| } | |
| // If object (rare cases) | |
| if (typeof text === "object") { | |
| if (text.innerText) return text.innerText.trim(); | |
| if (text.text) return text.text.trim(); | |
| return fallback; | |
| } | |
| // String → strip HTML safely | |
| if (typeof text === "string") { | |
| const cleaned = text | |
| .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "") | |
| .replace(/<[^>]*>/g, "") | |
| .replace(/\s+/g, " ") | |
| .trim(); | |
| return cleaned || fallback; | |
| } | |
| return fallback; | |
| }; | |
| const truncateUrl = (url = "", maxLength = 45) => { | |
| if (!url) return ""; | |
| if (url.length <= maxLength) return url; | |
| return url.slice(0, maxLength) + "…"; | |
| }; | |
| const onEditLink=(link)=>{ | |
| setWindowReady(true,link) | |
| } | |
| /** | |
| * Load all icon libraries together | |
| */ | |
| async function loadAllLibraries() { | |
| try { | |
| setLoading(true); | |
| const libraryFiles = Object.entries(ICON_LIBRARIES); // [id, config] | |
| const allData = await Promise.all( | |
| libraryFiles.map(([id, lib]) => | |
| fetch(`/${lib.dataFile}`).then(res => { | |
| if (!res.ok) throw new Error(`Failed to load ${lib.dataFile}`); | |
| return res.json().then(data => | |
| (data.icons || []).map(icon => ({ ...icon, library: id })) | |
| ); | |
| }) | |
| ) | |
| ); | |
| const mergedIcons = allData.flat(); // flatten array of arrays | |
| setIcons(mergedIcons); | |
| } catch (err) { | |
| console.error("Error loading all libraries:", err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| const toggleAccordion=(id)=>{ | |
| setActiveAccordion(prev=> prev===id ? null : id); | |
| } | |
| useEffect(() => { | |
| if (!selectedElement) return; | |
| const { instanceId, dataProp } = selectedElement; | |
| if (!instanceId || !dataProp) return; | |
| const selectedComp = componentsData.find(c => c.instanceId === instanceId); | |
| if (!selectedComp) return; | |
| const overrides = selectedComp.overrides || {}; | |
| const styleKey = `${dataProp}_style`; // Example: aText1_style | |
| const savedStyle = overrides[styleKey] || {}; | |
| // ✅ Now populate the fields from saved DB values | |
| setPropertyValues({ | |
| fontFamily: savedStyle.fontFamily || "inherit", | |
| fontSize: savedStyle.fontSize?.replace("px", "") || "", | |
| color: savedStyle.color || "#000000", | |
| backgroundColor: savedStyle.backgroundColor || "#ffffff", | |
| lineHeight: savedStyle.lineHeight?.replace("px", "") || "normal", | |
| letterSpacing: savedStyle.letterSpacing?.replace("px", "") || "normal", | |
| fontWeight: savedStyle.fontWeight || "400", | |
| fontStyle: savedStyle.fontStyle || "normal", | |
| textDecoration: savedStyle.textDecoration || "none", | |
| }); | |
| }, [selectedElement, componentsData]); | |
| const handleSearchChange = (e) => { | |
| const value = e.target.value; | |
| setSearchTerm(value); | |
| debouncedSetSearchTerm(value); | |
| setResetIcon(true); | |
| if(value===''){ | |
| setDebouncedSearchTerm(''); | |
| } | |
| }; | |
| const handleSaveImageMeta = () => { | |
| const alt = altText; | |
| const title = imageTitle; | |
| if (!selectedElement?.instanceId || !selectedElement?.dataProp) return; | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!targetIframe?.contentWindow) return; | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_IMAGE_META", | |
| instanceId: selectedElement.instanceId, | |
| dataProp: selectedElement.dataProp, | |
| alt, | |
| title, | |
| }, | |
| "*" | |
| ); | |
| }; | |
| const handleApplyVideoUrl = (e) => { | |
| e.stopPropagation(); | |
| if (!videoUrl.trim()) return alert("Please enter a video URL."); | |
| const url = videoUrl.trim(); | |
| const isYouTube = url.includes("youtube.com") || url.includes("youtu.be"); | |
| const isVimeo = url.includes("vimeo.com"); | |
| const isDirectVideo = url.endsWith(".mp4") || url.endsWith(".webm"); | |
| if (!isYouTube && !isVimeo && !isDirectVideo) { | |
| return alert("Invalid video URL. Supported: YouTube, Vimeo, Direct MP4/WebM."); | |
| } | |
| if (!selectedElement?.instanceId || !selectedElement?.dataProp) { | |
| return alert("No video element selected."); | |
| } | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| console.log('tagetIframe',targetIframe); | |
| if (!targetIframe?.contentWindow) return alert("Iframe not found."); | |
| // ✅ Send message to ws-script | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "APPLY_VIDEO_URL", | |
| instanceId: selectedElement.instanceId, | |
| dataProp: selectedElement.dataProp, | |
| videoUrl: url, | |
| }, | |
| "*" | |
| ); | |
| console.log("🎬 Sent APPLY_VIDEO_URL:", url); | |
| setAppliedUrl(url); | |
| }; | |
| // 🧠 Filtering icons dynamically | |
| const filterIcons = React.useMemo(() => { | |
| let list = [...icons]; | |
| console.log('list Printed',list); | |
| // 🔍 1. Search Synonyms Expand | |
| let searchKey = search.toLowerCase(); | |
| let searchWords = [ | |
| searchKey, | |
| ...(SEARCH_SYNONYMS[searchKey] || []) | |
| ]; | |
| // 🗂️ 2. Category Filter | |
| if (selectedCategory !== "all") { | |
| list = list.filter((icon) => icon.category === selectedCategory); | |
| } | |
| // 📚 3. Library Filter | |
| if (selectedLibrary !== "popular" && selectedLibrary !== "all") { | |
| list = list.filter((icon) => icon.library === selectedLibrary); | |
| } | |
| // 🔍 4. Search Filter (with synonyms) | |
| if (searchKey.trim() !== "") { | |
| list = list.filter((icon) => { | |
| const name = icon.name.toLowerCase(); | |
| return searchWords.some((word) => name.includes(word)); | |
| }); | |
| } | |
| return list; | |
| }, [icons, search, selectedLibrary, selectedCategory]); | |
| console.log('filterdIcons',filterIcons); | |
| const handleSelect = (icon) => { | |
| if (!icon?.name) return; | |
| let library = icon.library; | |
| if (icon.fontFamily) { | |
| if (icon.fontFamily.includes("Phosphor")) library = "ph"; | |
| } | |
| const safeIcon = { | |
| name: icon.name, | |
| library: library || "tabler", | |
| }; | |
| setSelectedIcon(safeIcon); | |
| // Include formatting meta | |
| setSelectedIconMeta({ | |
| type: "ICON", // ✅ Add type here | |
| color: currentColor, | |
| fontSize: `${currentSize}px`, | |
| rotation: currentRotation, | |
| flipHorizontal: isFlippedHorizontal, | |
| flipVertical: isFlippedVertical, | |
| fontFamily: "inherit", | |
| }); | |
| console.log("🎯 Icon selected:", safeIcon); | |
| }; | |
| // KEEP ICON META IN SYNC | |
| useEffect(() => { | |
| if (!selectedIcon) return; | |
| setSelectedIconMeta((prev) => ({ | |
| ...prev, | |
| color: currentColor, | |
| fontSize: `${currentSize}px`, | |
| rotation: currentRotation, | |
| flipHorizontal: isFlippedHorizontal, | |
| flipVertical: isFlippedVertical, | |
| fontFamily: "inherit", | |
| })); | |
| }, [ | |
| currentColor, | |
| currentSize, | |
| currentRotation, | |
| isFlippedHorizontal, | |
| isFlippedVertical, | |
| selectedIcon | |
| ]); | |
| const handleApply = () => { | |
| if (!selectedIcon || !selectedElement?.dataProp) return; | |
| const iframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!iframe?.contentWindow) return; | |
| const finalStyle = { | |
| color: currentColor, | |
| fill: currentColor, | |
| stroke: currentColor, | |
| fontSize: `${currentSize}px`, | |
| transform: ` | |
| rotate(${currentRotation}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| }; | |
| iframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_ICON_STYLE", | |
| payload: { | |
| dataProp: selectedElement.dataProp, | |
| style: finalStyle, | |
| icon: selectedIcon, | |
| }, | |
| }, | |
| "*" | |
| ); | |
| }; | |
| const pascalName = useMemo(() => { | |
| if (!selectedIcon?.name) return ""; | |
| return selectedIcon.name | |
| .split(/[-_]/) | |
| .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) | |
| .join(""); | |
| }, [selectedIcon?.name]); | |
| // =============================== | |
| // ICON PREVIEW DATA | |
| // =============================== | |
| const previewDataIcon = `${selectedIcon.library}:${selectedIcon.name}`; | |
| const previewDisplayName = selectedIcon.name; | |
| const previewLibraryName = getLibraryName(selectedIcon.library); // friendly name | |
| // Helper to convert icon name to PascalCase | |
| function toPascalCase(str) { | |
| return str | |
| .split(/[-_]/) | |
| .map(s => s.charAt(0).toUpperCase() + s.slice(1)) | |
| .join(""); | |
| } | |
| // Helper: Convert to embed URL for iframe | |
| const getEmbedUrl = (url) => { | |
| if (url.includes("youtube.com") || url.includes("youtu.be")) { | |
| const videoId = url.split("v=")[1]?.split("&")[0] || url.split("/").pop(); | |
| return `https://www.youtube.com/embed/${videoId}`; | |
| } else if (url.includes("vimeo.com")) { | |
| const videoId = url.split("/").pop(); | |
| return `https://player.vimeo.com/video/${videoId}`; | |
| } | |
| return url; // direct video file | |
| }; | |
| const handleBgFileChange = async (e) => { | |
| const file = e.target.files[0]; | |
| console.log('e printed',file); | |
| if (!file) return; | |
| await handleImageUploadBackground(file); | |
| }; | |
| const handleFileChange = async (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| await handleImageUpload(file); | |
| }; | |
| const handleImageUploadBackground = async (file) => { | |
| if (!selectedElement?.instanceId || !selectedElement?.dataProp) { | |
| console.warn("⚠️ No image selected"); | |
| return; | |
| } | |
| console.log("file printed in image upload", file); | |
| setUploadingBg(true); | |
| setProgress(0); | |
| try { | |
| const formData = new FormData(); | |
| formData.append("fileName", file.name); | |
| formData.append("fileType", file.type); | |
| formData.append("file", file); | |
| const res = await fetch( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/components/upload?instanceId=${selectedElement.instanceId}`, | |
| { | |
| method: "POST", | |
| headers: { | |
| "x-instance-id": selectedElement.instanceId, | |
| }, | |
| body: formData, | |
| } | |
| ); | |
| if (!res.ok) throw new Error(`Upload failed with ${res.status}`); | |
| const json = await res.json(); | |
| if (!json?.data?.url) throw new Error("No image URL returned"); | |
| const publicUrl = json.data.url; | |
| // Animate progress for UX | |
| for (let p = 10; p <= 90; p += 10) { | |
| await new Promise((r) => setTimeout(r, 80)); | |
| setProgress(p); | |
| } | |
| setProgress(100); | |
| await new Promise((r) => setTimeout(r, 800)); | |
| // Get iframe | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!targetIframe?.contentWindow) { | |
| console.warn("⚠️ Iframe not found for instance:", selectedElement.instanceId); | |
| return; | |
| } | |
| // 🧠 Detect if selected element is background-based or img tag | |
| const sectionType = selectedElement.section?.toLowerCase?.() || ""; | |
| if (sectionType === "background") { | |
| console.log("🌄 Updating background-image in iframe for:", selectedElement.dataProp); | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_BACKGROUND_IMAGE", | |
| instanceId: selectedElement.instanceId, | |
| dataProp: selectedElement.dataProp, | |
| fileUrl: publicUrl, | |
| style: { | |
| backgroundImage: `url('${publicUrl}')`, | |
| backgroundSize: "cover", | |
| backgroundPosition: "center center", | |
| backgroundRepeat: "no-repeat", | |
| }, | |
| }, | |
| "*" | |
| ); | |
| } | |
| } catch (err) { | |
| console.error("❌ Upload error:", err); | |
| setProgress(0); | |
| } finally { | |
| setUploadingBg(false); | |
| } | |
| }; | |
| const updateFormStyles = useCallback((override = {}) => { | |
| if (!selectedElement?.instanceId) return; | |
| setFormStyleState(prev => { | |
| // ⭐ FULL merged state (NO data loss) | |
| const merged = { ...prev, ...override }; | |
| // ⭐ Computed CSS from merged (NOT override) | |
| const style = {}; | |
| if (merged.maxWidth !== undefined) | |
| style.maxWidth = `${merged.maxWidth}px`; | |
| if (merged.padding !== undefined) | |
| style.padding = `${merged.padding}px`; | |
| if (merged.borderRadius !== undefined) | |
| style.borderRadius = `${merged.borderRadius}px`; | |
| if (merged.borderWidth !== undefined) { | |
| if (merged.borderWidth > 0) { | |
| style.borderWidth = `${merged.borderWidth}px`; | |
| style.borderStyle = "solid"; | |
| style.borderColor = merged.borderColor ?? formBorderColor; | |
| } else { | |
| style.borderWidth = "0px"; | |
| style.borderStyle = "none"; | |
| } | |
| } | |
| if (merged.borderColor !== undefined) | |
| style.borderColor = merged.borderColor; | |
| if (merged.shadow !== undefined) { | |
| style.boxShadow = [ | |
| "none", | |
| "0px 1px 3px rgba(0,0,0,0.12)", | |
| "0px 2px 6px rgba(0,0,0,0.16)", | |
| "0px 4px 12px rgba(0,0,0,0.20)" | |
| ][merged.shadow]; | |
| } | |
| const isGradient = v => typeof v === "string" && v.includes("gradient"); | |
| if (merged.bg !== undefined) { | |
| style.background = merged.bg?.trim() ? merged.bg : "none"; | |
| delete style.backgroundColor; | |
| } | |
| if (merged.bgColor !== undefined) { | |
| if (isGradient(merged.bgColor)) { | |
| style.background = merged.bgColor; | |
| delete style.backgroundColor; | |
| } else { | |
| style.backgroundColor = merged.bgColor; | |
| } | |
| } | |
| if (merged.alignment !== undefined) { | |
| style.margin = | |
| merged.alignment === "left" | |
| ? "0px auto 0px 0px" | |
| : merged.alignment === "center" | |
| ? "0px auto" | |
| : "0px 0px 0px auto"; | |
| } | |
| // ⭐ ALWAYS send full state | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (targetIframe) { | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "FORM_STYLE_UPDATE", | |
| instanceId: selectedElement.instanceId, | |
| payload: { | |
| ...merged, // ← ALWAYS FULL DATA | |
| ...style, | |
| } | |
| }, | |
| "*" | |
| ); | |
| } | |
| return merged; // ⭐ state preserved | |
| }); | |
| }, [formBorderColor, selectedElement, pageIframeRefs]); | |
| const handleImageUpload = async (file) => { | |
| if (!selectedElement?.instanceId || !selectedElement?.dataProp) { | |
| console.warn("⚠️ No image selected"); | |
| return; | |
| } | |
| console.log('file printed in image upload',file) | |
| setUploading(true); | |
| setProgress(0); | |
| try { | |
| const formData = new FormData(); | |
| formData.append("file", file); | |
| const res = await fetch( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/components/upload?instanceId=${selectedElement.instanceId}`, | |
| { | |
| method: "POST", | |
| headers: { | |
| "x-instance-id": selectedElement.instanceId, | |
| }, | |
| body: formData, | |
| } | |
| ); | |
| if (!res.ok) throw new Error(`Upload failed with ${res.status}`); | |
| const json = await res.json(); | |
| if (!json?.data?.url) throw new Error("No image URL returned"); | |
| const publicUrl = json.data.url; | |
| // Animate progress | |
| for (let p = 10; p <= 90; p += 10) { | |
| await new Promise((r) => setTimeout(r, 80)); | |
| setProgress(p); | |
| } | |
| setProgress(100); | |
| await new Promise((r) => setTimeout(r, 800)); | |
| // Send to iframe | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!targetIframe?.contentWindow) { | |
| console.warn("⚠️ Iframe not found for instance:", selectedElement.instanceId); | |
| return; | |
| } | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "UPLOAD_IMAGE", | |
| instanceId: selectedElement.instanceId, | |
| dataProp: selectedElement.dataProp, | |
| fileUrl: publicUrl, | |
| }, | |
| "*" | |
| ); | |
| } catch (err) { | |
| console.error("❌ Upload error:", err); | |
| setProgress(0); | |
| } finally { | |
| setUploading(false); | |
| } | |
| }; | |
| // 🔹 Handle file selection for video | |
| const handleVideoChange = async (e) => { | |
| console.log('e printed',e) | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| await handleVideoUpload(file); | |
| }; | |
| // 🔹 Upload video API call | |
| const handleVideoUpload = async (file) => { | |
| if (!selectedElement?.instanceId || !selectedElement?.dataProp) { | |
| console.warn("⚠️ No video selected"); | |
| return; | |
| } | |
| setUploading(true); | |
| setProgress(0); | |
| try { | |
| const formData = new FormData(); | |
| formData.append("fileName", file.name); | |
| formData.append("fileType", file.type); | |
| formData.append("file", file); | |
| const res = await fetch( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/components/upload?instanceId=${selectedElement.instanceId}`, | |
| { method: "POST", body: formData } | |
| ); | |
| if (!res.ok) throw new Error(`Upload failed with ${res.status}`); | |
| const json = await res.json(); | |
| if (!json?.data?.url) throw new Error("No video URL returned"); | |
| const publicUrl = json.data.url; | |
| // Animate progress | |
| for (let p = 10; p <= 90; p += 10) { | |
| await new Promise((r) => setTimeout(r, 80)); | |
| setProgress(p); | |
| } | |
| setProgress(100); | |
| await new Promise((r) => setTimeout(r, 800)); | |
| // Send video URL to iframe | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!targetIframe?.contentWindow) { | |
| console.warn("⚠️ Iframe not found for instance:", selectedElement.instanceId); | |
| return; | |
| } | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "UPLOAD_VIDEO", | |
| instanceId: selectedElement.instanceId, | |
| dataProp: selectedElement.dataVideoProp || selectedElement.dataProp, | |
| fileUrl: publicUrl, | |
| }, | |
| "*" | |
| ); | |
| } catch (err) { | |
| console.error("❌ Upload error:", err); | |
| setProgress(0); | |
| } finally { | |
| setUploading(false); | |
| } | |
| }; | |
| const applyFormattingText = (styleKey, value) => { | |
| if (!selectedElement?.dataProp || !selectedElement?.instanceId) { | |
| console.warn("⚠️ No selected element for formatting"); | |
| return; | |
| } | |
| // toggle support — if active, remove formatting instead | |
| setActiveFormats((prev) => { | |
| const isActive = prev[styleKey]; | |
| const newState = { ...prev, [styleKey]: !isActive }; | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!targetIframe?.contentWindow) { | |
| console.warn("⚠️ Target iframe not found for instanceId:", selectedElement.instanceId); | |
| return prev; | |
| } | |
| // Send message to iframe | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "APPLY_FORMATTING", | |
| dataProp: selectedElement.dataProp, | |
| instanceId: selectedElement.instanceId, | |
| style: { [styleKey]: isActive ? false : value }, // false = remove formatting | |
| }, | |
| "*" | |
| ); | |
| console.log( | |
| `${isActive ? "❌ Removing" : "✅ Applying"} formatting:`, | |
| styleKey | |
| ); | |
| return newState; | |
| }); | |
| }; | |
| const fontMap = { | |
| Arial: `"Arial", sans-serif`, | |
| Georgia: `"Georgia", serif`, | |
| "Times New Roman": `"Times New Roman", serif`, | |
| "Courier New": `"Courier New", monospace`, | |
| Inter: `"Inter", sans-serif`, | |
| Roboto: `"Roboto", sans-serif`, | |
| Poppins: `"Poppins", sans-serif`, | |
| "Open Sans": `"Open Sans", sans-serif`, | |
| "Plus Jakarta Sans": `"Plus Jakarta Sans", sans-serif`, | |
| Montserrat: `"Montserrat", sans-serif`, | |
| "General Sans": `"General Sans", sans-serif`, | |
| "Cabinet Grotesk": `"Cabinet Grotesk", sans-serif`, | |
| }; | |
| const applyFormatting = (styleKey, value) => { | |
| if (!selectedElement?.dataProp || !selectedElement?.instanceId) { | |
| console.warn("⚠️ No selected element for formatting"); | |
| return; | |
| } | |
| console.log("🟡 Applying formatting:", styleKey, value, selectedElement); | |
| const formattedValue = styleKey === "fontFamily" ? fontMap[value] || value : value; | |
| const targetIframe = | |
| pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!targetIframe?.contentWindow) { | |
| console.warn("⚠️ Target iframe not found for instanceId:", selectedElement.instanceId); | |
| return; | |
| } | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "APPLY_FORMATTING", | |
| dataProp: selectedElement.dataProp, | |
| instanceId: selectedElement.instanceId, | |
| style: { [styleKey]: formattedValue }, | |
| }, | |
| "*" | |
| ); | |
| console.log("📤 Message sent to iframe:", selectedElement.instanceId); | |
| }; | |
| const sendImageEffectEvent = (opacity, blur) => { | |
| if (!selectedElement?.dataProp || !selectedElement?.instanceId) return; | |
| const style = { | |
| opacity: opacity / 100, | |
| filter: `blur(${blur}px)`, | |
| }; | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!targetIframe?.contentWindow) return; | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "APPLY_IMAGE_EFFECTS", | |
| dataProp: selectedElement.dataProp, | |
| instanceId: selectedElement.instanceId, | |
| style, | |
| }, | |
| "*" | |
| ); | |
| }; | |
| const sendBgImageEffectEvent = (opacity, blur) => { | |
| if (!selectedElement?.dataProp || !selectedElement?.instanceId) return; | |
| const style = { | |
| opacity: opacity / 100, | |
| filter: `blur(${blur}px)`, | |
| }; | |
| const targetIframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!targetIframe?.contentWindow) return; | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "APPLY_IMAGE_EFFECTS", | |
| dataProp: selectedElement.dataProp, | |
| instanceId: selectedElement.instanceId, | |
| style, | |
| }, | |
| "*" | |
| ); | |
| }; | |
| useEffect(() => { | |
| console.log('property Value fontSize',propertyValues.fontSize) | |
| if (propertyValues.fontSize) { | |
| setFontSize(parseInt(propertyValues.fontSize, 10)); | |
| } | |
| }, [propertyValues.fontSize]); | |
| const handleFontSizeChange = (value) => { | |
| const size = Math.min(120, Math.max(8, value)); // clamp between 8–120 | |
| setFontSize(size); | |
| // ✅ Send to iframe for live HTML update | |
| applyFormatting("fontSize", `${size}px`); | |
| }; | |
| useEffect(() => { | |
| function handleClickOutside(event) { | |
| if (panelRef.current && !panelRef.current.contains(event.target)) { | |
| // agar bahar click hua hai to panel band karo | |
| setSliderToggle(false); | |
| } | |
| } | |
| document.addEventListener("mousedown", handleClickOutside); | |
| return () => { | |
| document.removeEventListener("mousedown", handleClickOutside); | |
| }; | |
| }, []); | |
| const updatePageById = async (e, pageId) => { | |
| setSaveText(true); | |
| e.preventDefault(); | |
| try { | |
| const authToken = localStorage.getItem("authToken"); | |
| if (!authToken) { | |
| router.push("/login"); | |
| return; | |
| } | |
| const decodedToken = jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (decodedToken.exp < currentTime) { | |
| localStorage.removeItem("authToken"); | |
| router.push("/login"); | |
| return; | |
| } | |
| // ✅ Always normalize order before sending | |
| const cleanedComponents = selectedComponentRef.current | |
| .sort((a, b) => a.order - b.order) | |
| .map((comp, index) => { | |
| const { type, order, mode, isNavbar, ...rest } = comp; | |
| const componentId = | |
| typeof comp.componentId === "object" && comp.componentId._id | |
| ? comp.componentId._id | |
| : comp.componentId; | |
| return { | |
| ...rest, | |
| order: index + 1, | |
| componentId, | |
| overrides: comp.overrides ?? {}, | |
| currentPath: "page-previews", | |
| }; | |
| }); | |
| console.log("🔹 Sending cleaned components:", cleanedComponents); | |
| const response = await axios.patch( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/${pageId}`, | |
| { components: cleanedComponents}, | |
| { headers: { Authorization: `${authToken}` } } | |
| ); | |
| if (response.data?.success) { | |
| const previewUrl = `${process.env.NEXT_PUBLIC_PREVIEW_BASE_URL}/page-previews/page-html/${pageId}.html`; | |
| window.open(previewUrl, "_blank"); | |
| setPublishButton(true); | |
| } | |
| } catch (error) { | |
| //console.error("❌ Error updating page:", error); | |
| } finally { | |
| setSaveText(false); | |
| } | |
| }; | |
| const updateSelectedPageById = async (e, pageId) => { | |
| e.preventDefault(); | |
| //console.log("pageId", pageId); | |
| //console.log("selected Components", selectedComponents); | |
| //console.log('selected COmponent Page',componentsPage); | |
| //console.log("component Refs", selectedPageComponentRef.current); | |
| setSaveText(true) | |
| try { | |
| const authToken = localStorage.getItem("authToken"); | |
| if (!authToken) { | |
| router.push("/login"); | |
| return; | |
| } | |
| const decodedToken = jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (decodedToken.exp < currentTime) { | |
| localStorage.removeItem("authToken"); | |
| router.push("/login"); | |
| return; | |
| } | |
| // ✅ Combine selectedComponents + selectedPageComponentRef without duplication | |
| const mergedComponentMap = new Map(); | |
| // Add from selectedPageComponentRef (used in memory) | |
| selectedPageComponentRef.current.forEach((comp) => { | |
| mergedComponentMap.set(comp.instanceId, comp); | |
| }); | |
| //console.log('total Add Component printed',totalAddComponent) | |
| // Overwrite or add from selectedComponents (usually latest UI state) | |
| if(totalAddComponent>0){ | |
| selectedComponents.forEach((comp) => { | |
| mergedComponentMap.set(comp.instanceId, comp); | |
| }); | |
| } | |
| //console.log('merged Component Maps',mergedComponentMap); | |
| const cleanComponents = [...componentsPage] | |
| .filter((comp) => { | |
| const id = | |
| typeof comp.componentId === "string" | |
| ? comp.componentId | |
| : comp.componentId?._id || comp._id; | |
| if (!id) { | |
| return false; | |
| } | |
| return true; | |
| }) | |
| .sort((a, b) => a.order - b.order) | |
| .map((comp) => ({ | |
| instanceId: comp.instanceId, | |
| componentId: | |
| typeof comp.componentId === "string" | |
| ? comp.componentId | |
| : comp.componentId?._id || comp._id, | |
| order: comp.order ?? 0, | |
| filePath: comp.filePath, | |
| currentPath: "page-previews", // ✅ add here | |
| })); | |
| // 🔁 Delay API call to ensure backend flush completes | |
| //console.log("🧾 Final Components to Save:", cleanComponents); | |
| const response = await axios.patch( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/${pageId}`, | |
| { | |
| components: cleanComponents, | |
| }, | |
| { | |
| headers: { | |
| Authorization: `${authToken}`, | |
| }, | |
| } | |
| ); | |
| if (response.data?.success) { | |
| //console.log("✅ Page updated successfully", response.data); | |
| //Open new tab with preview URL | |
| const previewUrl = `${process.env.NEXT_PUBLIC_PREVIEW_BASE_URL}/page-previews/page-html/${pageId}.html`; | |
| window.open(previewUrl, "_blank"); | |
| setPublishButton(true) | |
| } | |
| } catch (error) { | |
| //console.error("❌ updateSelectedPageById error", error); | |
| } | |
| finally{ | |
| setTimeout(()=>{ | |
| setSaveText(false) | |
| },1000) | |
| } | |
| }; | |
| const handleGetPageById = async (pageId) => { | |
| try { | |
| //console.log("🔥 API call pageId:", pageId); | |
| const authToken = localStorage.getItem("authToken"); | |
| if (!authToken) return router.push("/login"); | |
| const decodedToken = jwtDecode(authToken); | |
| if (decodedToken.exp < Math.floor(Date.now() / 1000)) { | |
| localStorage.removeItem("authToken"); | |
| return router.push("/login"); | |
| } | |
| const response = await axios.get( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/${pageId}`, | |
| { | |
| headers: { | |
| Authorization: `${authToken}`, | |
| }, | |
| } | |
| ); | |
| if (response.data?.success) { | |
| const rawComponents = response.data.data[0].components; | |
| // Normalize components for use across builder | |
| const normalized = rawComponents | |
| .slice() // copy to avoid mutation | |
| .sort((a, b) => { | |
| const aOrder = a.order ?? 0; | |
| const bOrder = b.order ?? 0; | |
| return aOrder - bOrder; | |
| }) | |
| .map((comp, index) => ({ | |
| ...comp, | |
| componentId: | |
| typeof comp.componentId === "string" | |
| ? { _id: comp.componentId, filePath: comp.filePath || "" } | |
| : comp.componentId, | |
| mode: "page", | |
| overrides: comp.overrides || {}, | |
| order: comp.order ?? index, // fallback still okay | |
| })); | |
| // ✅ Set all builder states | |
| setComponentsPage(normalized); | |
| setComponentsData(normalized); // if builder depends on this too | |
| setPageComponent(normalized.map((c) => c.componentId)); | |
| // ✅ MOST IMPORTANT: hydrate ref used by WebSocket handler | |
| selectedPageComponentRef.current = normalized; | |
| //console.log("✅ Page Components initialized", normalized); | |
| } | |
| } catch (error) { | |
| //console.log("❌ Error loading page data", error); | |
| } | |
| }; | |
| const handleGenerateZip = async (websiteId, pageId) => { | |
| try { | |
| const authToken = localStorage.getItem("authToken"); | |
| if (!authToken) return router.push("/login"); | |
| const decodedToken = jwtDecode(authToken); | |
| if (decodedToken.exp < Math.floor(Date.now() / 1000)) { | |
| localStorage.removeItem("authToken"); | |
| return router.push("/login"); | |
| } | |
| const response = await axios.post( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/render/website`, | |
| { websiteId, pageId }, | |
| { headers: { Authorization: authToken } } | |
| ); | |
| if (response.data?.success) { | |
| const resData = response.data.data[0]; | |
| const subdomainValue = resData.subdomain; | |
| const homepageId = resData.homepageId; | |
| setSubdomain(subdomainValue); | |
| const currentPage = fetchedPages.find(p => p._id === pageId); | |
| console.log("Current Page:", currentPage); | |
| // ✅ Filename rule matches backend | |
| const redirectFileName = | |
| currentPage._id === homepageId | |
| ? "index.html" | |
| : `${(currentPage.slug || currentPage._id) | |
| .toString() | |
| .toLowerCase()}.html`; | |
| console.log("Redirect File Name:", redirectFileName); | |
| // ✅ Set filename (empty means homepage) | |
| setRenderFileName(redirectFileName === "index.html" ? "" : redirectFileName); | |
| // ✅ Open modal | |
| setPublishModal(true); | |
| } else { | |
| setPublishModalError(true); | |
| } | |
| } catch (error) { | |
| console.error("❌ Publish Error:", error.response?.data || error.message); | |
| setPublishModalError(true); | |
| } finally { | |
| setTimeout(() => setPublishModalError(false), 1500); | |
| } | |
| }; | |
| //console.log('selected COmponent Page ref After',selectedPageComponentRef); | |
| useEffect(() => { | |
| const map = {}; | |
| componentsPage.forEach((comp) => { | |
| map[comp.instanceId] = comp.order; | |
| }); | |
| orderMapPageRef.current = map; | |
| }, [componentsPage]); | |
| //generate Static Site | |
| useEffect(() => { | |
| const handleOpenPageModal = (pageId) => { | |
| //console.log('openPageModal') | |
| //console.log('pageid', pageId) | |
| setPageId(pageId) | |
| handleGetPageById(pageId) | |
| } | |
| //console.log('components Page Printed',componentsPage); | |
| eventBus.on('openPageModal', handleOpenPageModal) | |
| return () => { | |
| eventBus.off('openPageModal', handleOpenPageModal) | |
| } | |
| }, []) | |
| useEffect(() => { | |
| const searchParams = new URLSearchParams(window.location.search); | |
| const urlPageId = searchParams.get("pageId"); | |
| if (urlPageId) { | |
| setPageId(urlPageId); | |
| setTimeout(() => { | |
| eventBus.emit("openPageModal", urlPageId); | |
| window.history.replaceState({}, "", window.location.pathname); | |
| }, 50); | |
| } | |
| }, []); | |
| useEffect(() => { | |
| const fetchPagesOnBuilderLoad = async () => { | |
| try{ | |
| const authToken = localStorage.getItem('authToken') | |
| if (!authToken) return | |
| const res = await axios.get(`${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/`, { | |
| headers: { Authorization: authToken } | |
| }) | |
| const fetchedPages = res.data.data[0].pages; | |
| //console.log('fetched Pages',fetchedPages); | |
| if (res.data?.success) { | |
| const pages = res.data.data[0]?.pages || []; | |
| setFetchedPages(pages); | |
| // ✅ Get websiteId from the first page (assuming all pages belong to same website) | |
| if (pages.length > 0 && pages[0].websiteId) { | |
| setWebsiteId(pages[0].websiteId); | |
| } | |
| } | |
| } | |
| catch(error){ | |
| if(error.response.data?.message=="Missing required fields: websiteId"){ | |
| setWebsiteModal(true); | |
| } | |
| } | |
| } | |
| fetchPagesOnBuilderLoad(); | |
| }, []) | |
| const handleGetSchemas=async()=>{ | |
| try{ | |
| const authToken=localStorage.getItem('authToken'); | |
| if(!authToken){ | |
| router.push('/login'); | |
| return; | |
| } | |
| const decodedToken=jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if(decodedToken.exp<currentTime){ | |
| localStorage.removeItem('authToken'); | |
| router.push('/login'); | |
| return; | |
| } | |
| const response=await axios.get(`${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/schemas/`,{ | |
| headers:{ | |
| Authorization:`${authToken}` | |
| } | |
| }) | |
| if(response.data?.success){ | |
| const data=response.data.data[0].schemas; | |
| const filterData=data.filter(item=> | |
| item.status==='ACTIVE' || item.status==='DRAFT' | |
| ); | |
| //console.log('data printed',filterData); | |
| setSchemas(filterData); | |
| } | |
| } | |
| catch(error){ | |
| if(error.response.data?.message==="Missing required fields: websiteId, databaseId"){ | |
| setWebsiteModal(true) | |
| } | |
| } | |
| } | |
| useEffect(()=>{ | |
| handleGetSchemas(); | |
| },[]) | |
| //sync ref on every state change | |
| // ✅ useEffect to run on search term change | |
| useEffect(() => { | |
| handleGetComponents(); | |
| }, [debouncedSearchTerm]); | |
| //console.log('componentsPage printed',componentsPage); | |
| const sortedComponentsPage = useMemo(() => { | |
| return [...componentsPage].sort((a, b) => a.order - b.order); | |
| }, [componentsPage]); | |
| // useEffect(()=>{ | |
| // console.log('componentsPage Printed',componentsPage) | |
| // },[]) | |
| //console.log('iframe SRC Memo',iframeSrcMap); | |
| // Include pageId for correct dynamic src | |
| // ------------------- selectedLinkProps ------------------- | |
| const selectedLinkProps = useMemo(() => { | |
| const extractLinks = (arr = []) => { | |
| const results = []; | |
| arr.forEach((comp) => { | |
| if (!comp?.overrides) return; | |
| const isNav = comp.type?.toLowerCase() === "navbar" || comp.isNavbar; | |
| Object.keys(comp.overrides) | |
| .filter((k) => k.startsWith("aHref")) | |
| .forEach((hrefKey) => { | |
| const index = hrefKey.replace("aHref", ""); | |
| const textKey = `aText${index}`; | |
| const text = comp.overrides[textKey] || ""; | |
| const href = comp.overrides[hrefKey] || ""; | |
| results.push({ | |
| instanceId: comp.instanceId, | |
| key: index, | |
| href, | |
| text, | |
| isNav, | |
| }); | |
| }); | |
| }); | |
| // 🟢 Merge manual edits | |
| // 🟢 Merge manual edits (FIXED) | |
| if (manualEditedLinksRef.current) { | |
| Object.entries(manualEditedLinksRef.current).forEach(([instKey, data]) => { | |
| Object.entries(data).forEach(([key, value]) => { | |
| const existing = results.find( | |
| (r) => r.instanceId === instKey && r.key === key | |
| ); | |
| if (existing) { | |
| // ✅ empty string "" allow hogi | |
| if ("href" in value) { | |
| existing.href = value.href; | |
| } | |
| if ("text" in value) { | |
| existing.text = value.text; | |
| } | |
| } else { | |
| results.push({ | |
| instanceId: instKey, | |
| key, | |
| href: value.href ?? "", | |
| text: value.text ?? "", | |
| }); | |
| } | |
| }); | |
| }); | |
| } | |
| return results; | |
| }; | |
| // 🟢 FIX: Page data ko preference di | |
| const merged = [ | |
| ...(Array.isArray(sortedComponentsPage) ? sortedComponentsPage : []), // page first | |
| ...(Array.isArray(componentsData) ? componentsData : []), // then global | |
| ].filter( | |
| (comp, index, self) => | |
| self.findIndex((c) => c.instanceId === comp.instanceId) === index | |
| ); | |
| return extractLinks(merged); | |
| }, [componentsData, sortedComponentsPage]); | |
| // useEffect(()=>{ | |
| // console.log('manually Edited Links',manuallyEditedLinks) | |
| // console.log('selected Link Props in Effect',selectedLinkProps) | |
| // },[manuallyEditedLinks,selectedLinkProps]) | |
| // console.log('selected Link Props in Memos',selectedLinkProps) | |
| // console.log('componentsData',componentsData); | |
| const postPageComponents = async (componentsArray) => { | |
| try { | |
| const authToken = localStorage.getItem("authToken"); | |
| if (!authToken) { | |
| router.push('/login'); | |
| return []; | |
| } | |
| const decodedToken = jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (decodedToken.exp < currentTime) { | |
| localStorage.removeItem("authToken"); | |
| router.push("/login"); | |
| return []; | |
| } | |
| const filePathResults = []; | |
| for (const componentData of componentsArray) { | |
| const result = await axios.post( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/post-component`, | |
| componentData, | |
| { | |
| headers: { | |
| Authorization: `${authToken}`, | |
| }, | |
| } | |
| ); | |
| const components = result?.data?.data?.[0]?.components || []; | |
| const targetComp = components.find(c => c.instanceId === componentData.instanceId); | |
| if (targetComp?.filePath) { | |
| filePathResults.push({ | |
| instanceId: componentData.instanceId, | |
| filePath: targetComp.filePath, | |
| }); | |
| } | |
| } | |
| return filePathResults; | |
| } catch (error) { | |
| //console.log("error in postPageComponents", error); | |
| return []; | |
| } | |
| }; | |
| const buildFilePath = (filePath, instanceId) => { | |
| const isFullURL = filePath.startsWith("http://") || filePath.startsWith("https://"); | |
| // 🔥 Strip any previous query params | |
| const baseFilePath = filePath.split("?")[0]; | |
| return isFullURL | |
| ? `${baseFilePath}?instanceId=${instanceId}&mode=local` | |
| : `${process.env.NEXT_PUBLIC_PAGE_PREVIEW_BASE_URL}/${baseFilePath}?instanceId=${instanceId}&mode=local`; | |
| }; | |
| //console.log('selected Link Props After',selectedLinkProps) | |
| const connectWebSocket = () => { | |
| const existing = persistentWSRef.current; | |
| // Already connected | |
| if (existing && existing.readyState === WebSocket.OPEN) return; | |
| // Clear previous heartbeat | |
| if (heartbeatRef.current) clearInterval(heartbeatRef.current); | |
| // Close existing WS if not closed | |
| if (existing && existing.readyState !== WebSocket.CLOSED) existing.close(); | |
| const ws = new WebSocket(process.env.NEXT_PUBLIC_WEBSOCKET_URL); | |
| persistentWSRef.current = ws; | |
| wsRef.current = ws; | |
| let reconnectAttempt = 0; // exponential backoff | |
| ws.onopen = () => { | |
| //console.log("✅ WebSocket connected"); | |
| // Heartbeat every 25s | |
| heartbeatRef.current = setInterval(() => { | |
| if (ws.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ type: "ping" })); | |
| } | |
| }, 25000); | |
| reconnectAttempt = 0; // reset attempts on successful connect | |
| }; | |
| ws.onclose = () => { | |
| clearInterval(heartbeatRef.current); | |
| reconnectAttempt++; | |
| const delay = Math.min(30000, 2000 * reconnectAttempt); // max 30s | |
| //console.log(`🔄 Reconnecting WS in ${delay / 1000}s (attempt ${reconnectAttempt})`); | |
| setTimeout(() => { | |
| connectWebSocket(); // retry | |
| }, delay); | |
| }; | |
| ws.onerror = (err) => { | |
| //console.error("❌ WebSocket error:", err); | |
| ws.close(); // triggers onclose | |
| }; | |
| }; | |
| function cleanNumber(value) { | |
| if (value === undefined || value === null) return 0; | |
| const cleaned = String(value) | |
| .replace(/px/gi, "") | |
| .replace(/%/gi, "") | |
| .replace(/[^\d.]/g, ""); | |
| console.log('cleaned Number',cleaned); | |
| return cleaned === "" ? 0 : Number(cleaned); | |
| } | |
| const updateSelectedLinkProps = (updatedLink) => { | |
| setSelectedLinkProps((prev) => | |
| prev.map((link) => | |
| link.instanceId === updatedLink.instanceId && | |
| link.key === updatedLink.key | |
| ? { ...link, ...updatedLink } | |
| : link | |
| ) | |
| ); | |
| }; | |
| // 📡 WebSocket connect | |
| useEffect(() => { | |
| connectWebSocket(); | |
| const handleMessage = (event) => { | |
| const { type, instanceId, height, overrides, htmlContent, mode,propName,value } = event.data || {}; | |
| //console.log("📩 handleMessage received:", type, { instanceId, mode, overrides }); | |
| // ---------------- iframeHeight ---------------- | |
| if (type === "iframeHeight") { | |
| const pageIframe = pageIframeRefs.current?.[instanceId]; | |
| if (pageIframe) { | |
| pageIframe.style.height = `${height}px`; | |
| //console.log(`📏 Updated iframe height for ${instanceId}: ${height}px`); | |
| } | |
| return; | |
| } | |
| // ---------------- UPDATE_COMPONENT_DATA ---------------- | |
| if (type === "UPDATE_COMPONENT_DATA") { | |
| const rawOverrides = overrides || {}; | |
| //console.log("🛠 UPDATE_COMPONENT_DATA rawOverrides:", rawOverrides); | |
| const updateRef = (ref) => | |
| Array.isArray(ref) | |
| ? ref.map((comp) => { | |
| if (comp.instanceId !== instanceId) return comp; | |
| const newOverrides = { ...(comp.overrides || {}), ...rawOverrides }; | |
| //console.log("🔄 Merging overrides for instance:", instanceId, newOverrides); | |
| return { ...comp, overrides: newOverrides }; | |
| }) | |
| : ref && ref.instanceId === instanceId | |
| ? { ...ref, overrides: { ...(ref.overrides || {}), ...rawOverrides } } | |
| : ref; | |
| // ✅ Update main componentsData | |
| setComponentsData((prev) => { | |
| const updated = updateRef(prev); | |
| //console.log("✅ Updated componentsData:", updated); | |
| return updated; | |
| }); | |
| selectedComponentRef.current = updateRef(selectedComponentRef.current); | |
| //console.log("✅ Updated selectedComponentRef.current:", selectedComponentRef.current); | |
| // ✅ Always update componentsPage + refs (irrespective of mode) | |
| setComponentsPage((prev) => { | |
| const updated = prev.map((c) => | |
| c.instanceId === instanceId | |
| ? { ...c, overrides: { ...(c.overrides || {}), ...rawOverrides } } | |
| : c | |
| ); | |
| //console.log("✅ Updated setComponentsPage:", updated); | |
| return updated; | |
| }); | |
| componentsPageRef.current = updateRef(componentsPageRef.current); | |
| selectedPageComponentRef.current = updateRef(selectedPageComponentRef.current); | |
| // console.log("✅ Synced page refs:", { | |
| // componentsPageRef: componentsPageRef.current, | |
| // selectedPageComponentRef: selectedPageComponentRef.current, | |
| // }); | |
| // ✅ Extract compType from updated data instead of stale state | |
| const updatedComponents = updateRef(componentsData); | |
| const compType = | |
| (Array.isArray(updatedComponents) | |
| ? updatedComponents.find((c) => c.instanceId === instanceId)?.type | |
| : updatedComponents?.type) || ""; | |
| //console.log(`📌 CompType for ${instanceId}:`, compType); | |
| // Send update via WS | |
| if (wsRef.current?.readyState === WebSocket.OPEN) { | |
| wsRef.current.send( | |
| JSON.stringify({ | |
| type: "overrideUpdate", | |
| pageId, | |
| instanceId, | |
| overrides: rawOverrides, | |
| mode, | |
| compType, | |
| currentPath: window.location.pathname, | |
| }) | |
| ); | |
| // console.log("📤 Sent overrideUpdate via WebSocket:", { | |
| // pageId, | |
| // instanceId, | |
| // overrides: rawOverrides, | |
| // mode, | |
| // compType, | |
| // }); | |
| } | |
| return; | |
| } | |
| // ---------------- ELEMENT_SELECTED ---------------- | |
| if (type === "ELEMENT_SELECTED") { | |
| const { section, meta, dataProp, instanceId } = event.data; | |
| console.log('section data:', section, meta); | |
| // detect if it's the same element clicked again | |
| const isSameElement = | |
| selectedElement?.dataProp === dataProp && | |
| selectedElement?.instanceId === instanceId; | |
| setSelectedElement({ | |
| section, | |
| dataProp: event.data.dataProp, | |
| instanceId: event.data.instanceId, | |
| }); | |
| setSliderToggle(true); | |
| setSliders('sliders'); | |
| setSelectedComponentInstanceId(event.data.instanceId); | |
| // 🧹 Reset text section to default values every time a new element is clicked | |
| if (section === "text" && !isSameElement && ActiveSection!=='icon') { | |
| setFontSize(16); | |
| setFontFamily("inherit"); | |
| setTextColor("#000000"); | |
| setLineHeight("1.5"); | |
| setLetterSpacing("0"); | |
| setFontWeight("normal"); | |
| setFontStyle("normal"); | |
| setTextDecoration("none"); | |
| setTextContent(""); | |
| setPropertyValues({ | |
| fontFamily: "inherit", | |
| color: "#000000", | |
| backgroundColor: "#ffffff", | |
| }); | |
| setLineHeightValue(1.5); | |
| setLetterSpacingValue(0); | |
| // 🧩 Add this 👇 | |
| setActiveFormats({ | |
| bold: false, | |
| italic: false, | |
| underline: false, | |
| strike: false, | |
| superscript: false, | |
| subscript: false, | |
| }); | |
| } | |
| if(section==='background' && !isSameElement && ActiveSection!=='icon'){ | |
| if(ActiveSection==='icon') return | |
| setFontSize(16); | |
| setFontFamily("inherit"); | |
| setTextColor("#000000"); | |
| setLineHeight("1.5"); | |
| setLetterSpacing("0"); | |
| setFontWeight("normal"); | |
| setFontStyle("normal"); | |
| setTextDecoration("none"); | |
| setTextContent(""); | |
| setPropertyValues({ | |
| fontFamily: "inherit", | |
| color: "#000000", | |
| backgroundColor: "#ffffff", | |
| }); | |
| setLineHeightValue(1.5); | |
| setLetterSpacingValue(0); | |
| // 🧩 Add this 👇 | |
| setActiveFormats({ | |
| bold: false, | |
| italic: false, | |
| underline: false, | |
| strike: false, | |
| superscript: false, | |
| subscript: false, | |
| }); | |
| } | |
| console.log('Active Section:', section); | |
| console.log('Meta Received:', meta); | |
| // 📝 Text section | |
| if (section === "text" && meta) { | |
| // Apply styles from meta | |
| if (meta.fontSize) setFontSize(parseInt(meta.fontSize, 10)); | |
| if (meta.fontFamily) setFontFamily(meta.fontFamily); | |
| if (meta.color) setTextColor(meta.color); | |
| if (meta.lineHeight) setLineHeight(meta.lineHeight); | |
| if (meta.letterSpacing) setLetterSpacing(meta.letterSpacing); | |
| if (meta.fontWeight) setFontWeight(meta.fontWeight); | |
| if (meta.fontStyle) setFontStyle(meta.fontStyle); | |
| if (meta.textDecoration) setTextDecoration(meta.textDecoration); | |
| if (meta.innerHTML) setTextContent(meta.innerHTML.trim()); | |
| // ✅ Use detectedFormats from meta directly | |
| const detectedFormats = meta.detectedFormats || { | |
| bold: false, | |
| italic: false, | |
| underline: false, | |
| strike: false, | |
| superscript: false, | |
| subscript: false, | |
| }; | |
| console.log("detected Formats", detectedFormats); | |
| // 🧩 Only update if changed or new element | |
| setActiveFormats((prev) => { | |
| const changed = | |
| !isSameElement || | |
| Object.keys(detectedFormats).some( | |
| (key) => prev[key] !== detectedFormats[key] | |
| ); | |
| return changed ? detectedFormats : prev; | |
| }); | |
| } | |
| // 🖼️ Image section | |
| if (section === "image" && meta) { | |
| if (meta.alt !== undefined) setAltText(meta.alt); | |
| if (meta.title !== undefined) setImageTitle(meta.title); | |
| if (meta.opacity !== undefined && meta.opacity !== null) { | |
| setImageOpacity(Number(meta.opacity)); | |
| } | |
| if (meta.blur !== undefined && meta.blur !== null) { | |
| setImageBlur(Number(meta.blur)); | |
| } | |
| } | |
| // 🌄 Background Image section | |
| if (section === "background" && meta) { | |
| // Apply styles from meta | |
| if (meta.fontSize) setFontSize(parseInt(meta.fontSize, 10)); | |
| if (meta.fontFamily) setFontFamily(meta.fontFamily); | |
| if (meta.color) setTextColor(meta.color); | |
| if (meta.lineHeight) setLineHeight(meta.lineHeight); | |
| if (meta.letterSpacing) setLetterSpacing(meta.letterSpacing); | |
| if (meta.fontWeight) setFontWeight(meta.fontWeight); | |
| if (meta.fontStyle) setFontStyle(meta.fontStyle); | |
| if (meta.textDecoration) setTextDecoration(meta.textDecoration); | |
| if (meta.innerHTML) setTextContent(meta.innerHTML.trim()); | |
| // ✅ Use detectedFormats from meta directly | |
| const detectedFormats = meta.detectedFormats || { | |
| bold: false, | |
| italic: false, | |
| underline: false, | |
| strike: false, | |
| superscript: false, | |
| subscript: false, | |
| }; | |
| console.log("detected Formats", detectedFormats); | |
| // 🧩 Only update if changed or new element | |
| setActiveFormats((prev) => { | |
| const changed = | |
| !isSameElement || | |
| Object.keys(detectedFormats).some( | |
| (key) => prev[key] !== detectedFormats[key] | |
| ); | |
| return changed ? detectedFormats : prev; | |
| }); | |
| if (meta.src) setBgImageSrc(meta.src); // URL of background image | |
| if (meta.opacity !== undefined && meta.opacity !== null) setBgImageOpacity(Number(meta.opacity)); | |
| if (meta.blur !== undefined && meta.blur !== null) setBgImageBlur(Number(meta.blur)); | |
| } | |
| // 🎥 VIDEO HANDLING | |
| if (section=== "VIDEO") { | |
| const sourceEl = el.querySelector("source"); | |
| const directSrc = el.getAttribute("src") || ""; | |
| const finalSrc = sourceEl?.getAttribute("src") || directSrc || ""; | |
| // Ensure single source consistency | |
| if (!sourceEl && finalSrc) { | |
| const newSource = document.createElement("source"); | |
| newSource.setAttribute("src", finalSrc); | |
| newSource.setAttribute("type", "video/mp4"); | |
| el.appendChild(newSource); | |
| } else if (sourceEl && directSrc && sourceEl.getAttribute("src") !== directSrc) { | |
| sourceEl.setAttribute("src", directSrc); | |
| } | |
| // 🔁 Sync both src attributes | |
| if (sourceEl && !el.getAttribute("src")) el.setAttribute("src", sourceEl.getAttribute("src")); | |
| if (el.getAttribute("src") && sourceEl && sourceEl.getAttribute("src") !== el.getAttribute("src")) { | |
| sourceEl.setAttribute("src", el.getAttribute("src")); | |
| } | |
| const poster = el.getAttribute("poster") || ""; | |
| const autoplay = el.hasAttribute("autoplay"); | |
| const loop = el.hasAttribute("loop"); | |
| const muted = el.hasAttribute("muted"); | |
| const controls = el.hasAttribute("controls"); | |
| const style = { | |
| width: el.style.width || "", | |
| height: el.style.height || "", | |
| objectFit: el.style.objectFit || "", | |
| }; | |
| newOverrides[key] = { | |
| src: finalSrc, | |
| poster, | |
| autoplay, | |
| loop, | |
| muted, | |
| controls, | |
| style, | |
| videoProp: el.getAttribute("data-video-prop") || "", | |
| videoType: el.getAttribute("data-video-type") || "", | |
| }; | |
| console.log("[sendUpdate] 🎬 Video override:", newOverrides[key]); | |
| return; | |
| } | |
| // ---------------- Form Handling ---------------- | |
| if (section === "form" && meta) { | |
| // Apply styles from meta | |
| if (meta.fontSize) setFontSize(parseInt(meta.fontSize, 10)); | |
| if (meta.fontFamily) setFontFamily(meta.fontFamily); | |
| if (meta.color) setTextColor(meta.color); | |
| if (meta.lineHeight) setLineHeight(meta.lineHeight); | |
| if (meta.letterSpacing) setLetterSpacing(meta.letterSpacing); | |
| if (meta.fontWeight) setFontWeight(meta.fontWeight); | |
| if (meta.fontStyle) setFontStyle(meta.fontStyle); | |
| if (meta.textDecoration) setTextDecoration(meta.textDecoration); | |
| if (meta.innerHTML) setTextContent(meta.innerHTML.trim()); | |
| // ✅ Use detectedFormats from meta directly | |
| const detectedFormats = meta.detectedFormats || { | |
| bold: false, | |
| italic: false, | |
| underline: false, | |
| strike: false, | |
| superscript: false, | |
| subscript: false, | |
| }; | |
| console.log("detected Formats", detectedFormats); | |
| // 🧩 Only update if changed or new element | |
| setActiveFormats((prev) => { | |
| const changed = | |
| !isSameElement || | |
| Object.keys(detectedFormats).some( | |
| (key) => prev[key] !== detectedFormats[key] | |
| ); | |
| return changed ? detectedFormats : prev; | |
| }); | |
| const safe = { | |
| formId: meta.formId || dataProp, | |
| title: meta.title || "", | |
| description: meta.description || "", | |
| padding: cleanNumber(meta.padding), | |
| borderRadius: cleanNumber(meta.borderRadius), | |
| maxWidth: cleanNumber(meta.maxWidth), | |
| alignment: meta.alignment || "left", | |
| // ⭐ Background | |
| background: meta.background || "", | |
| backgroundColor: meta.backgroundColor || "", | |
| // ⭐ Border | |
| borderWidth: cleanNumber(meta.borderWidth), | |
| borderColor: meta.borderColor || "", | |
| borderStyle: meta.borderStyle || "solid", | |
| // ⭐ Shadow | |
| boxShadow: meta.boxShadow || "none", | |
| }; | |
| // ⭐ Apply to UI states | |
| if (safe.background !== undefined) setFormBackground(safe.background); | |
| if (safe.backgroundColor !== undefined) setFormBackgroundColor(safe.backgroundColor); | |
| if (!isNaN(safe.borderWidth)) setFormBorderWidth(safe.borderWidth); | |
| if (safe.borderColor) setFormBorderColor(safe.borderColor); | |
| if (safe.borderStyle) setFormBorderStyle(safe.borderStyle); | |
| // ⭐ Shadow intensity decode | |
| if (safe.boxShadow === "none") setFormShadow(0); | |
| else if (safe.boxShadow.includes("0px 1px")) setFormShadow(1); | |
| else if (safe.boxShadow.includes("0px 2px")) setFormShadow(2); | |
| else setFormShadow(3); | |
| // ⭐ Basic styles | |
| if (!isNaN(safe.padding)) setFormPadding(safe.padding); | |
| if (!isNaN(safe.borderRadius)) setFormBorderRadius(safe.borderRadius); | |
| if (!isNaN(safe.maxWidth)) setFormMaxWidth(safe.maxWidth); | |
| // Alignment | |
| setFormAlignment(safe.alignment); | |
| // Metadata | |
| setFormId(safe.formId); | |
| setFormTitle(safe.title); | |
| setFormDescription(safe.description); | |
| } | |
| // 🎨 ICON HANDLING | |
| if (section === "icon" && meta) { | |
| if (!isSameElement) { | |
| setSelectedIcon({ | |
| name: meta.name, | |
| library: meta.library, | |
| classList: meta.classList || [], | |
| }); | |
| setSelectedIconMeta({ | |
| type: "ICON", | |
| color: meta.color || "#000000", | |
| fontSize: meta.fontSize || "16px", | |
| rotation: meta.rotation || "0", | |
| }); | |
| // ⭐⭐⭐ THIS IS THE IMPORTANT FIX ⭐⭐⭐ | |
| setCurrentColor(meta.color || "#000000"); | |
| setCurrentSize(meta.fontSize || "16px"); | |
| setCurrentRotation(meta.rotation || 0); | |
| setIsFlippedHorizontal(meta.flipHorizontal || false); | |
| setIsFlippedVertical(meta.flipVertical || false); | |
| } | |
| } | |
| setActiveSection(section.toLowerCase()); // ✅ move this up | |
| } | |
| // ---------------- PROPERTY_UPDATE (NEW direct update type) ---------------- | |
| if (type === "PROPERTY_UPDATE") { | |
| if (!propName || value === undefined) return; | |
| // Update main data | |
| setComponentsData((prev) => | |
| prev.map((comp) => | |
| comp.instanceId === instanceId | |
| ? { | |
| ...comp, | |
| overrides: { ...(comp.overrides || {}), [propName]: value }, | |
| } | |
| : comp | |
| ) | |
| ); | |
| // Keep references synced | |
| if (selectedPageComponentRef.current?.instanceId === instanceId) { | |
| selectedPageComponentRef.current = { | |
| ...selectedPageComponentRef.current, | |
| overrides: { | |
| ...(selectedPageComponentRef.current.overrides || {}), | |
| [propName]: value, | |
| }, | |
| }; | |
| } | |
| return; | |
| } | |
| // ---------------- updateComponent ---------------- | |
| if (type === "updateComponent") { | |
| const iframeDoc = pageIframeRefs.current[instanceId]?.contentDocument; | |
| const root = iframeDoc?.getElementById("editable-root"); | |
| if (root && htmlContent) { | |
| root.innerHTML = htmlContent; | |
| //console.log(`🖊 Updated component DOM for ${instanceId}`); | |
| } | |
| } | |
| }; | |
| window.addEventListener("message", handleMessage); | |
| return () => { | |
| clearInterval(heartbeatRef.current); | |
| window.removeEventListener("message", handleMessage); | |
| if (wsRef.current) { | |
| wsRef.current.onopen = null; | |
| wsRef.current.onclose = null; | |
| wsRef.current.onerror = null; | |
| wsRef.current.close(); | |
| } | |
| }; | |
| }, [pageId]); | |
| useEffect(() => { | |
| if (selectedIcon) { | |
| console.log("Updated selected icon library:", selectedIcon.library); | |
| } | |
| }, [selectedIcon]); | |
| // ---------------- Reconnect event listener ---------------- | |
| useEffect(() => { | |
| const reconnectHandler = () => connectWebSocket(); | |
| window.addEventListener("reconnect-ws", reconnectHandler); | |
| return () => window.removeEventListener("reconnect-ws", reconnectHandler); | |
| }, []); | |
| useEffect(() => { | |
| componentsPageRefState.current = componentsPage; | |
| //console.log('componentsPage ref crrenr',componentsPageRefState.current) | |
| }, [componentsPage]); | |
| //console.log('components Pagess',componentsPage); | |
| //console.log('page iframe ref',pageIframeRefs); | |
| const handleDragStart = (e, instanceId) => { | |
| e.dataTransfer.setData("text/plain", instanceId); | |
| setMovingId(instanceId); | |
| }; | |
| const syncSelectedComponents = (newComponents) => { | |
| const cleanedComponents = newComponents.map(comp => ({ | |
| componentId: comp.componentId, | |
| instanceId: comp.instanceId, | |
| order: orderMapRef.current[comp.instanceId] ?? comp.order, | |
| overrides: comp.overrides, | |
| })); | |
| setSelectedComponents(cleanedComponents); | |
| selectedComponentRef.current = cleanedComponents; | |
| }; | |
| const handleMoveUp = (instanceId) => { | |
| setComponentsData(prevComponents => { | |
| const index = prevComponents.findIndex(c => c.instanceId === instanceId); | |
| if (index <= 0) return prevComponents; | |
| const updated = [...prevComponents]; | |
| const temp = updated[index - 1]; | |
| updated[index - 1] = updated[index]; | |
| updated[index] = temp; | |
| // Optional: reassign order if used visually or for saving | |
| return updated.map((c, i) => ({ ...c, order: i })); | |
| }); | |
| }; | |
| const handleMoveDown = (instanceId) => { | |
| setComponentsData(prevComponents => { | |
| const index = prevComponents.findIndex(c => c.instanceId === instanceId); | |
| if (index === -1 || index >= prevComponents.length - 1) return prevComponents; | |
| const updated = [...prevComponents]; | |
| const temp = updated[index + 1]; | |
| updated[index + 1] = updated[index]; | |
| updated[index] = temp; | |
| // Optional: reassign order if needed | |
| return updated.map((c, i) => ({ ...c, order: i })); | |
| }); | |
| }; | |
| const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); | |
| const syncComponentsPage = (sourceList = sortedComponentsPage) => { | |
| const prevRef = componentsPageRef.current || []; | |
| let didChange = false; | |
| const updated = sourceList.map((block, index) => { | |
| const prev = componentObjectMapRef.current[block.instanceId] || {}; | |
| const newOverrides = { | |
| ...(prev.overrides || {}), | |
| ...(block.overrides || {}), | |
| }; | |
| const merged = { | |
| ...prev, | |
| ...block, | |
| overrides: newOverrides, | |
| }; | |
| if (!deepEqual(prev, merged)) { | |
| didChange = true; | |
| componentObjectMapRef.current[block.instanceId] = merged; // only when changed | |
| } | |
| return componentObjectMapRef.current[block.instanceId]; | |
| }); | |
| const isOrderChanged = | |
| prevRef.length !== updated.length || | |
| prevRef.some((c, i) => c.instanceId !== updated[i].instanceId); | |
| if (!didChange && !isOrderChanged) { | |
| //console.log("✅ No content/order change — skipping state"); | |
| return; | |
| } | |
| componentsPageRef.current = updated; | |
| selectedPageComponentRef.current = updated; | |
| setComponentsPage((prev) => { | |
| if ( | |
| prev.length === updated.length && | |
| prev.every((c, i) => c.instanceId === updated[i].instanceId && deepEqual(c, updated[i])) | |
| ) { | |
| //console.log("✅ Skipping state update — identical"); | |
| return prev; | |
| } | |
| //console.log("⏫ Updating componentsPage — actual changes"); | |
| return updated; | |
| }); | |
| }; | |
| const handleMovePageComponentUp = (instanceId) => { | |
| setComponentsPage(prev => { | |
| const list = structuredClone(prev); | |
| const index = list.findIndex(c => c.instanceId === instanceId); | |
| if (index <= 0) return prev; | |
| [list[index - 1], list[index]] = [list[index], list[index - 1]]; | |
| // Check if actually changed | |
| if (list[index].instanceId === prev[index].instanceId) { | |
| return prev; // no actual reorder | |
| } | |
| list.forEach((c, i) => { | |
| c.order = i; | |
| orderMapPageRef.current[c.instanceId] = i; | |
| }); | |
| componentsPageRef.current = list; | |
| selectedPageComponentRef.current = list; | |
| return list; | |
| }); | |
| }; | |
| const handleMovePageComponentDown = (instanceId) => { | |
| setComponentsPage(prev => { | |
| const list = structuredClone(prev); | |
| const index = list.findIndex(c => c.instanceId === instanceId); | |
| if (index <= 0) return prev; | |
| [list[index + 1], list[index]] = [list[index], list[index + 1]]; | |
| // Check if actually changed | |
| if (list[index].instanceId === prev[index].instanceId) { | |
| return prev; // no actual reorder | |
| } | |
| list.forEach((c, i) => { | |
| c.order = i; | |
| orderMapPageRef.current[c.instanceId] = i; | |
| }); | |
| componentsPageRef.current = list; | |
| selectedPageComponentRef.current = list; | |
| return list; | |
| }); | |
| }; | |
| //console.log('components printed in main',components); | |
| //console.log('componentsPage Printed',componentsPage); | |
| //console.log('components Page Ref',componentsPageRef); | |
| const handleDragOver = (e) => { | |
| e.preventDefault(); | |
| e.dataTransfer.dropEffect = "move"; | |
| }; | |
| const handleDrop = (e, dropInstanceId) => { | |
| e.preventDefault(); | |
| const draggedInstanceId = e.dataTransfer.getData("text/plain"); | |
| if (!draggedInstanceId || draggedInstanceId === dropInstanceId) return; | |
| const currentOrderMap = [...selectedComponentRef.current]; | |
| //console.log('current Order Map',currentOrderMap); | |
| const draggedIndex = currentOrderMap.findIndex(c => c.instanceId === draggedInstanceId); | |
| const dropIndex = currentOrderMap.findIndex(c => c.instanceId === dropInstanceId); | |
| //console.log('drop Index',draggedIndex); | |
| //console.log('drop Index',dropIndex); | |
| if (draggedIndex === -1 || dropIndex === -1) return; | |
| // Remove dragged component and re-insert at new position | |
| const [draggedComp] = currentOrderMap.splice(draggedIndex, 1); | |
| currentOrderMap.splice(dropIndex, 0, draggedComp); | |
| // Reassign order based on new sequence | |
| const updated = currentOrderMap.map((comp, i) => ({ | |
| ...comp, | |
| order: i, | |
| })); | |
| //console.log('file Path Updated results',updated); | |
| selectedComponentRef.current = updated; | |
| setSelectedComponents(updated); | |
| // ✅ Force re-render | |
| setComponentsData(updated) | |
| //console.log("📦 Drag drop reorder applied"); | |
| }; | |
| const handlePageDragStart = (e, instanceId) => { | |
| e.dataTransfer.setData("text/plain", instanceId); | |
| setMovingPageId(instanceId) | |
| }; | |
| const handlePageDragOver = (e) => { | |
| e.preventDefault(); | |
| e.dataTransfer.dropEffect = "move"; | |
| }; | |
| const handlePageDrop = (e, dropInstanceId) => { | |
| e.preventDefault(); | |
| const draggedInstanceId = e.dataTransfer.getData("text/plain"); | |
| if (draggedInstanceId === dropInstanceId) return; | |
| setComponentsPage((prevComponents) => { | |
| const draggedComp = prevComponents.find((c) => c.instanceId === draggedInstanceId); | |
| const droppedComp = prevComponents.find((c) => c.instanceId === dropInstanceId); | |
| if (!draggedComp || !droppedComp) return prevComponents; | |
| const dragOrder = draggedComp.order; | |
| const dropOrder = droppedComp.order; | |
| const updatedList = prevComponents.map((comp) => { | |
| if (comp.instanceId === draggedInstanceId) return { ...comp, order: dropOrder }; | |
| if (comp.instanceId === dropInstanceId) return { ...comp, order: dragOrder }; | |
| return comp; | |
| }); | |
| const sortedUpdatedList = [...updatedList].sort((a, b) => a.order - b.order); | |
| // 🔁 Update refs/state with filtered version to avoid re-adding deleted components | |
| selectedComponentRefPage.current = selectedComponentRefPage.current | |
| .map((c) => { | |
| const updated = sortedUpdatedList.find((u) => u.instanceId === c.instanceId); | |
| return updated || null; | |
| }) | |
| .filter(Boolean); | |
| setSelectedComponents((prevSelected) => | |
| sortedUpdatedList.filter((c)=>{ | |
| prevSelected.some((p)=>p.instanceId===c.instanceId) | |
| }) | |
| ); | |
| return sortedUpdatedList; | |
| }); | |
| }; | |
| const moveComponentWithFeedback = (fromIndex, toIndex, compId) => { | |
| setMovingId(compId); // show "Moving Component..." | |
| moveComponent(fromIndex, toIndex); | |
| // reset after short delay so user can see the message | |
| setMovingId(null) | |
| }; | |
| const movePageComponentWithFeedback = (fromIndex, toIndex, compId) => { | |
| setMovingPageId(compId); // show feedback | |
| movePageComponent(fromIndex, toIndex); | |
| // reset after short delay so user can see the message | |
| setMovingPageId(null); | |
| }; | |
| const movePageComponent = (fromIndex, toIndex) => { | |
| const updated = [...sortedComponentsPage]; | |
| const [moved] = updated.splice(fromIndex, 1); | |
| updated.splice(toIndex, 0, moved); | |
| const reordered = updated.map((comp, i) => ({ ...comp, order: i })); | |
| setComponentsPage(reordered); | |
| selectedComponentRefPage.current = reordered; | |
| }; | |
| // const ComponentItem = React.memo(({ comp, onDragStart, onDragOver, onDrop, onDragEnd, deleteComponent, setShowModel, setPositionForNewComponent, iframeRefs, handleIframeLoad }) => { | |
| // return ( | |
| // <div | |
| // key={comp.instanceId} | |
| // id={`component-${comp.instanceId}`} | |
| // className={styles["canvas-component"]} | |
| // draggable | |
| // onDragStart={(e) => onDragStart(e, comp.instanceId)} | |
| // onDragOver={onDragOver} | |
| // onDrop={(e) => onDrop(e, comp.instanceId)} | |
| // onDragEnd={onDragEnd} | |
| // style={{ width: "100%", position: "relative", order: comp.order }} | |
| // > | |
| // <div className={styles["drag-handle"]}><DotsSixIcon /></div> | |
| // <button | |
| // className={styles["add-component-top"]} | |
| // onClick={() => { | |
| // setShowModel(true); | |
| // setPositionForNewComponent(`top:${comp.instanceId}`); | |
| // }} | |
| // >+</button> | |
| // <button | |
| // className={styles["delete-component-btn"]} | |
| // onClick={() => deleteComponent(comp.instanceId)} | |
| // ><TrashIcon /></button> | |
| // <div id={`iframe-container-${comp.instanceId}`} className={styles["component-content"]}> | |
| // <iframe | |
| // ref={(el) => { if (el) iframeRefs.current[comp.instanceId] = el }} | |
| // id={`iframe-${comp.instanceId}`} | |
| // src={comp.url} | |
| // style={{ width: '100%', border: 'none', minHeight: '50px', height: '1px' }} | |
| // sandbox="allow-scripts" | |
| // onLoad={() => handleIframeLoad(comp.instanceId)} | |
| // /> | |
| // </div> | |
| // <button | |
| // className={styles["add-component-bottom"]} | |
| // onClick={() => { | |
| // setShowModel(true); | |
| // setPositionForNewComponent(`bottom:${comp.instanceId}`); | |
| // }} | |
| // >+</button> | |
| // </div> | |
| // ) | |
| // }); | |
| //console.log('components',components); | |
| //console.log(components.map(c => c.instanceId)) | |
| const handleIframeLoad = (instanceId) => { | |
| const iframe =pageIframeRefs.current[instanceId]; | |
| if (!iframe) return; | |
| const comp = componentsData.find(c => c.instanceId === instanceId); | |
| if (!comp) return; | |
| //console.log("✅ iframe loaded for", instanceId); | |
| //console.log("📤 Sending overrides to iframe:", comp.overrides); | |
| //console.log('Overrides',overrides) | |
| try { | |
| iframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_COMPONENT_DATA", | |
| source: "page-builder", | |
| overrides: comp.overrides || {}, | |
| }, | |
| "*" | |
| ); | |
| } catch (err) { | |
| //console.error("❌ Error posting to iframe", err); | |
| } | |
| //loader hide | |
| setLoadingStates((prev)=>({ | |
| ...prev, | |
| [instanceId]:false | |
| })) | |
| }; | |
| const handleIframeLoadPage = (instanceId) => { | |
| const iframe = pageIframeRefs.current[instanceId]; | |
| if (!iframe) return; | |
| const comp = componentsPage.find(c => c.instanceId === instanceId); | |
| if (!comp) return; | |
| //console.log("✅ iframe loaded for", instanceId); | |
| //console.log("📤 Sending overrides to iframe:", comp.overrides); | |
| try { | |
| iframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_COMPONENT_DATA", | |
| source: "page-builder", | |
| overrides: comp.overrides || {}, | |
| }, | |
| "*" | |
| ); | |
| } catch (err) { | |
| //console.error("❌ Error posting to iframe", err); | |
| } | |
| //loader hide | |
| setLoadingStatesPage((prev)=>({ | |
| ...prev, | |
| [instanceId]: false, | |
| })); | |
| }; | |
| const redirectToWebsite=()=>{ | |
| router.push('/websites') | |
| } | |
| const handleDragEnd = () => { | |
| setMovingId(null) | |
| document.querySelectorAll("iframe").forEach(iframe => iframe.style.pointerEvents = "auto"); | |
| }; | |
| const moveComponent = (fromIndex, toIndex) => { | |
| const updated = [...componentsData]; | |
| const [moved] = updated.splice(fromIndex, 1); | |
| updated.splice(toIndex, 0, moved); | |
| const reordered = updated.map((comp, i) => ({ ...comp, order: i })); | |
| setComponentsData(reordered); | |
| selectedComponentRef.current = reordered; | |
| }; | |
| const handleFloatVisible = (target) => { | |
| if (activeTab === target) { | |
| //console.log('active tae',activeTab); | |
| // Same tab click kiya — to toggle close | |
| setToggle(false); | |
| setActivetab(null); | |
| } else { | |
| // Dusra tab click kiya — to open panel with new tab | |
| setActivetab(target); | |
| setToggle(true); | |
| } | |
| }; | |
| const handleRightVisible=(target)=>{ | |
| //console.log('sliders Printed',sliders); | |
| console.log('target printed',target) | |
| setSliders(target); | |
| setSliderToggle(true) | |
| } | |
| const handleComponentSelect = (component) => { | |
| const newInstanceId = uuidv4(); | |
| justAddedComponentRef.current=newInstanceId | |
| const newComp = { | |
| instanceId: newInstanceId, // naya | |
| componentId: component._id, // yeh global ka actual id | |
| overrides: {}, | |
| pageId, | |
| url: `${process.env.NEXT_PUBLIC_PREVIEW_BASE_URL}/pages/${pageId}/${newInstanceId}.html`, | |
| position: componentsPage.length + 1, | |
| }; | |
| // Add to componentsPage | |
| setComponentsPage(prev => { | |
| let updated = [...prev]; | |
| if (positionForNewComponent) { | |
| const [pos, targetId] = positionForNewComponent.split(":"); | |
| const index = updated.findIndex(c => c.instanceId === targetId); | |
| if (index !== -1) { | |
| if (pos === "top") updated.splice(index, 0, newComp); | |
| else updated.splice(index + 1, 0, newComp); | |
| } else { | |
| updated.push(newComp); | |
| } | |
| } else { | |
| updated.push(newComp); | |
| } | |
| return updated.map((c, i) => ({ ...c, position: i + 1 })); | |
| }); | |
| setShowModel(false); | |
| setPositionForNewComponent(null); | |
| }; | |
| // ---------------------- Component Click Handler ---------------------- | |
| const handleComponentClick = async (component) => { | |
| if (!component?._id) return; | |
| //console.log("🟢 Component clicked:", component); | |
| const instanceId = `inst-${uuidv4()}`; | |
| const overrides = {}; | |
| // Prepare override values | |
| component.editableProps?.forEach((prop) => { | |
| overrides[prop.key] = prop.default || ""; | |
| }); | |
| const currentPath = window.location.pathname; | |
| console.log('currentpath Printed',currentPath); | |
| //console.log("🛠 Prepared overrides:", overrides); | |
| const payload = [{ componentId: component._id, instanceId, pageId,currentPath }]; | |
| const results = await postPageComponents(payload); | |
| //console.log("📩 Payload results from backend:", results); | |
| const result = results.find((r) => r.instanceId === instanceId); | |
| const filePath = result?.filePath; | |
| if (!filePath) { | |
| //console.error("⚠️ File path not received for component:", component._id); | |
| return; | |
| } | |
| const newComponent = { | |
| instanceId, | |
| componentId: component._id, | |
| filePath: `${filePath}&compType=${encodeURIComponent(component.type || "generic")}`, | |
| order: 0, // will be normalized later | |
| overrides, | |
| type: component.type || "generic", | |
| isNavbar: component.isNavbar || false, | |
| mode: "local", | |
| }; | |
| //console.log("✨ New component object prepared:", newComponent); | |
| const insertComponent = (list, comp, position) => { | |
| let updated = [...list]; | |
| const total = updated.length; | |
| if (!position || position === "empty") { | |
| comp.order = total + 1; | |
| updated.push(comp); | |
| } else if (position.includes(":")) { | |
| const [pos, targetId] = position.split(":"); | |
| const index = updated.findIndex((c) => c.instanceId === targetId); | |
| if (index === -1) return updated; | |
| comp.order = | |
| pos === "top" ? updated[index].order - 0.5 : updated[index].order + 0.5; | |
| updated.push(comp); | |
| } else { | |
| comp.order = total + 1; | |
| updated.push(comp); | |
| } | |
| // ✅ Normalize sequential order | |
| return updated | |
| .sort((a, b) => a.order - b.order) | |
| .map((c, idx) => ({ ...c, order: idx + 1 })); | |
| }; | |
| // Update componentsData | |
| setComponentsData((prev) => { | |
| const updated = insertComponent(prev, newComponent, positionForNewComponent); | |
| // ✅ Keep ref in same normalized order | |
| selectedComponentRef.current = updated.map((c) => ({ ...c })); | |
| //console.log("📝 componentsData updated:", updated); | |
| return updated; | |
| }); | |
| // Update componentsPage if it exists | |
| if (componentsPage?.length > 0) { | |
| setComponentsPage((prev) => { | |
| const updated = insertComponent(prev, newComponent, positionForNewComponent); | |
| // ✅ Keep ref in same normalized order | |
| selectedPageComponentRef.current = updated.map((c) => ({ ...c })); | |
| //console.log("📝 componentsPage updated:", updated); | |
| return updated; | |
| }); | |
| } | |
| setTotalAddComponent((prev) => prev + 1); | |
| //console.log("✅ Total components added:", totalAddComponent + 1); | |
| setShowModel(false); | |
| //console.log("🚪 Modal closed after adding component."); | |
| }; | |
| const selectedLink = useMemo(() => { | |
| if (!selectedLinkId) return null; | |
| return selectedLinkProps.find( | |
| l => | |
| l.instanceId === selectedLinkId.instanceId && | |
| l.key === selectedLinkId.key | |
| ); | |
| }, [selectedLinkId, selectedLinkProps]); | |
| const handleSaveSelectedLink = () => { | |
| if (!selectedLink) return; | |
| const instanceId = selectedLink.instanceId; | |
| const iframe = pageIframeRefs.current[instanceId]; | |
| if (!iframe?.contentWindow) return; | |
| const idKey = `${instanceId}-${selectedLink.key}`; | |
| const finalHref = | |
| linkInputCache[idKey] !== undefined | |
| ? linkInputCache[idKey] | |
| : selectedLink.href; | |
| const finalText = | |
| linkInputCache[`${idKey}-text`] !== undefined | |
| ? linkInputCache[`${idKey}-text`] | |
| : selectedLink.text; | |
| const finalTarget = | |
| manualEditedLinksRef.current?.[idKey]?.target; | |
| // helper 🔁 (single override sender) | |
| const sendOverride = (overrideKey, overrideValue) => { | |
| // componentsData | |
| setComponentsData(prev => | |
| prev.map(comp => | |
| comp.instanceId === instanceId | |
| ? { | |
| ...comp, | |
| overrides: { | |
| ...comp.overrides, | |
| [overrideKey]: overrideValue, | |
| }, | |
| } | |
| : comp | |
| ) | |
| ); | |
| // componentsPage | |
| if (componentsPage?.length) { | |
| setComponentsPage(prev => | |
| prev.map(comp => | |
| comp.instanceId === instanceId | |
| ? { | |
| ...comp, | |
| overrides: { | |
| ...comp.overrides, | |
| [overrideKey]: overrideValue, | |
| }, | |
| } | |
| : comp | |
| ) | |
| ); | |
| } | |
| // iframe sync ✅ SAME AS WORKING CODE | |
| iframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_COMPONENT_DATA", | |
| source: "parent", | |
| instanceId, | |
| overrides: { [overrideKey]: overrideValue }, | |
| mode: "local", | |
| }, | |
| "*" | |
| ); | |
| }; | |
| if (finalHref !== undefined) { | |
| sendOverride(`aHref${selectedLink.key}`, finalHref); | |
| } | |
| if (finalText !== undefined) { | |
| sendOverride(`aText${selectedLink.key}`, finalText); | |
| } | |
| if (finalTarget !== undefined) { | |
| sendOverride(`aTarget${selectedLink.key}`, finalTarget); | |
| } | |
| setWindowReady(false); | |
| }; | |
| // useEffect(()=>{ | |
| // console.log('components data',componentsData) | |
| // },[componentsData]) | |
| //console.log('total Add Components',totalAddComponent); | |
| //console.log('link Input Cache Start',linkInputCache) | |
| //console.log('Position for New Components',positionForNewComponent); | |
| //console.log('selected Components',selectedComponents); | |
| //console.log('components printed in main',components); | |
| //console.log('componentsPage Printed',componentsPage); | |
| //console.log('components Page Ref',componentsPageRef); | |
| //console.log('Page Iframe Refs message printed',pageIframeRefs); | |
| const deleteSchemaById=async(e,schemaId)=>{ | |
| //console.log('schema',schemaId); | |
| e.preventDefault(); | |
| try{ | |
| const authToken=localStorage.getItem('authToken'); | |
| if(!authToken){ | |
| router.push('/login') | |
| return; | |
| } | |
| const decodedToken=jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (decodedToken.exp < currentTime) { | |
| localStorage.removeItem("authToken"); | |
| router.push("/login"); | |
| return; | |
| } | |
| const response=await axios.patch(`${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/schemas/status/${schemaId}`, | |
| {}, | |
| { | |
| headers:{ | |
| Authorization:`${authToken}` | |
| } | |
| }) | |
| if(response.data?.success){ | |
| //console.log('response datq printed',response.data.data); | |
| setSchemas((prevSchema)=>{ | |
| //console.log('prevsSchema',prevSchema); | |
| return prevSchema.filter((schema)=>{ | |
| //console.log('schemaId',schemaId); | |
| return schema._id!==schemaId | |
| }); | |
| } | |
| ); | |
| setDatabaseModal(false) | |
| setdeleteDatabaseToast(true); | |
| setTimeout(()=>{ | |
| setdeleteDatabaseToast(false) | |
| },5000); | |
| } | |
| } | |
| catch(error){ | |
| //console.log('error printed',error); | |
| } | |
| } | |
| const redirectPageCreation=()=>{ | |
| router.push('/page'); | |
| } | |
| const showDatatbaseModal=(e,schemaTableName,schemaTableId)=>{ | |
| e.preventDefault(); | |
| setTableName(schemaTableName); | |
| setSchemaId(schemaTableId); | |
| setDatabaseModal(true) | |
| } | |
| const handleClonePage=async(e,pageId)=>{ | |
| e.stopPropagation(); | |
| try{ | |
| const authToken=localStorage.getItem('authToken'); | |
| if(!authToken){ | |
| router.push('/login'); | |
| return; | |
| } | |
| const decodedToken=jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if(decodedToken.exp<currentTime){ | |
| localStorage.removeItem('authToken'); | |
| router.push('/login'); | |
| return; | |
| } | |
| const response=await axios.post(`${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/clone`, | |
| {pageId}, | |
| { | |
| headers:{ | |
| Authorization:`${authToken}` | |
| } | |
| }) | |
| if(response.data?.success){ | |
| //console.log('response data printed',response.data?.success); | |
| const clonedPage=response.data.data[0]; | |
| //console.log('reponse printed',clonedPage); | |
| const index=pages.findIndex(p=>p._id===pageId); | |
| //console.log('index',index); | |
| if(index!==-1){ | |
| const updatedPages = [ | |
| ...pages.slice(0, index + 1), | |
| clonedPage, | |
| ...pages.slice(index + 1) | |
| ]; | |
| setPages(updatedPages); | |
| setTotastModal(true); | |
| setTimeout(()=>{ | |
| setTotastModal(false) | |
| },5000) | |
| } | |
| } | |
| } | |
| catch(error){ | |
| //console.log('error printed',error); | |
| } | |
| } | |
| const addComponent=(newComponentUrl,positon)=>{ | |
| setComponents((prev)=>{ | |
| if(positon==='empty'){ | |
| return [{id:Date.now(),url:newComponentUrl}] | |
| } | |
| const [pos,targetId]=positon.split(":"); | |
| const index=prev.findIndex((c)=>c.id===targetId); | |
| //console.log('index',index); | |
| if(index===-1) return prev; | |
| const newComponent={id:Date.now(),url:newComponentUrl}; | |
| const updated=[...prev]; | |
| if(pos==='top') updated.splice(index,0,newComponent); | |
| else updated.splice(index+1,0,newComponent); | |
| return updated; | |
| }) | |
| } | |
| const updateSelectedComponents = () => { | |
| setSelectedComponents([...selectedComponentRef.current]); | |
| }; | |
| const deleteComponent =async (instanceId) => { | |
| //console.log("instanceId", instanceId); | |
| try{ | |
| const authToken=localStorage.getItem('authToken'); | |
| if(!authToken){ | |
| router.push('/login'); | |
| return; | |
| } | |
| const decodedToken = jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (decodedToken.exp < currentTime) { | |
| localStorage.removeItem("authToken"); | |
| router.push("/login"); | |
| return; | |
| } | |
| const response=await axios.delete(`${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/delete/${pageId}/components/${instanceId}`, | |
| { | |
| headers:{ | |
| Authorization:`${authToken}` | |
| } | |
| } | |
| ) | |
| if(response.data.success){ | |
| delete pageIframeRefs.current[instanceId]; | |
| delete heightCache.current[instanceId]; | |
| setComponentsData((prev) =>{ | |
| const filtered= prev.filter((c) => c.instanceId !== instanceId) | |
| // Remove from ref | |
| selectedComponentRef.current = selectedComponentRef.current.filter( | |
| (c) => c.instanceId !== instanceId | |
| ); | |
| setSelectedComponents(selectedComponentRef.current); | |
| return filtered; | |
| }) | |
| } | |
| } | |
| catch(error){ | |
| //console.log('error message printed',error); | |
| } | |
| }; | |
| //console.log('selected Component Printed After deleted',selectedComponents); | |
| const deletePageComponent = async(instanceId) => { | |
| try{ | |
| const authToken=localStorage.getItem('authToken'); | |
| if(!authToken){ | |
| router.push('/login'); | |
| return; | |
| } | |
| const decodedToken=jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (decodedToken.exp < currentTime) { | |
| localStorage.removeItem("authToken"); | |
| router.push("/login"); | |
| return; | |
| } | |
| const response=await axios.delete(`${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/delete/${pageId}/components/${instanceId}`,{ | |
| headers:{ | |
| Authorization:`${authToken}` | |
| } | |
| }) | |
| if(response.data.success){ | |
| delete pageIframeRefs.current[instanceId]; | |
| delete heightCache.current[instanceId]; | |
| setComponentsPage((prev) => prev.filter((c) => c.instanceId !== instanceId)); | |
| setSelectedComponents((prev) => prev.filter((c) => c.instanceId !== instanceId)); | |
| setComponentsData((prev) => prev.filter((c) => c.instanceId !== instanceId)); // ✅ Fix here | |
| selectedPageComponentRef.current = selectedPageComponentRef.current.filter((c) => c.instanceId !== instanceId); | |
| selectedComponentRef.current = selectedComponentRef.current.filter((c) => c.instanceId !== instanceId); // ✅ Also here | |
| } | |
| } | |
| catch(error){ | |
| //console.log('error message printed',error); | |
| } | |
| }; | |
| //Get Pages by Id | |
| const handleDeletePage=async(pageId)=>{ | |
| //console.log('pageId',pageId); | |
| try{ | |
| const authToken=localStorage.getItem('authToken'); | |
| if(!authToken){ | |
| router.push('/login'); | |
| return; | |
| } | |
| const decodedToken=jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if(decodedToken.exp<currentTime){ | |
| localStorage.removeItem('authToken'); | |
| router.push('/login'); | |
| return; | |
| } | |
| const response=await axios.delete(`${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/pages/${pageId}`,{ | |
| headers:{ | |
| Authorization:`${authToken}` | |
| } | |
| }) | |
| //console.log('response printed',response); | |
| if(response.data?.success){ | |
| //console.log('response data ',response.data.data); | |
| const updatedPages = pages.filter(p => p._id !== pageId); | |
| setPages(updatedPages); | |
| setpageDeleteModal(false); | |
| setDeletePageToast(true) | |
| setTimeout(()=>{ | |
| setDeletePageToast(false) | |
| },5000) | |
| } | |
| } | |
| catch(error){ | |
| //console.log('filter printed',error); | |
| } | |
| } | |
| const iconMap = { | |
| LAYOUT: Layout, | |
| CARD: Cards, // or Card if that’s the actual icon name | |
| // add other types as needed | |
| }; | |
| const cloneSchema = async (e, schemaId) => { | |
| e.preventDefault(); | |
| try { | |
| const authToken = localStorage.getItem("authToken"); | |
| if (!authToken) { | |
| router.push("/login"); | |
| return; | |
| } | |
| const decodedToken = jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (decodedToken.exp < currentTime) { | |
| localStorage.removeItem("authToken"); | |
| router.push("/login"); | |
| return; | |
| } | |
| const response = await axios.post( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/schemas/clone`, | |
| { schemaId }, | |
| { | |
| headers: { | |
| Authorization: `${authToken}`, | |
| }, | |
| } | |
| ); | |
| if (response.data?.success) { | |
| const clonedSchema = response.data.data[0]; | |
| //console.log("cloned schema:", clonedSchema); | |
| // Find original schema index | |
| const index = schemas.findIndex((s) => s._id === schemaId); | |
| if (index !== -1) { | |
| const updatedSchemas = [ | |
| ...schemas.slice(0, index + 1), | |
| clonedSchema, // insert cloned one here | |
| ...schemas.slice(index + 1), | |
| ]; | |
| setSchemas(updatedSchemas); | |
| setTotastModal(false) | |
| setDatabaseToastModal(true); | |
| setTimeout(()=>{ | |
| setDatabaseToastModal(false) | |
| },5000); | |
| } | |
| } | |
| } catch (error) { | |
| //console.log("error printed", error); | |
| } | |
| }; | |
| const redirectToTableView=(tableNameCreate,schmeaId)=>{ | |
| useRoleStore.getState().setTableName(tableNameCreate) | |
| router.push(`/view-table?schemaId=${schmeaId}`) | |
| } | |
| const handleGetComponents = async () => { | |
| try { | |
| setLoadingComponents(true) | |
| const authToken = localStorage.getItem("authToken"); | |
| if (!authToken) { | |
| router.push("/login"); | |
| return; | |
| } | |
| const decodedToken = jwtDecode(authToken); | |
| const currentTime = Math.floor(Date.now() / 1000); | |
| if (decodedToken.exp < currentTime) { | |
| localStorage.removeItem("authToken"); | |
| router.push("/login"); | |
| return; | |
| } | |
| // ⚡️ Build params object | |
| const params = {}; | |
| if (debouncedSearchTerm) { | |
| params.searchTerm = debouncedSearchTerm; | |
| } | |
| const response = await axios.get( | |
| `${process.env.NEXT_PUBLIC_CLIENT_PANEL_API_URL}/components/all`, | |
| { | |
| headers: { | |
| Authorization: `${authToken}`, | |
| }, | |
| params, // 👈 final query params | |
| } | |
| ); | |
| //console.log("response data printed", response.data); | |
| if (response.data?.success) { | |
| const data = response.data.data; | |
| if (!data.length) { | |
| //console.log("No components to fetch."); | |
| setComponentBy({}); // clear | |
| return; | |
| } | |
| // 👉 If search, data is flat array, else typewise | |
| if (debouncedSearchTerm) { | |
| // Flat array → ek dummy SEARCH_RESULTS type me | |
| setComponentBy({ SEARCH_RESULTS: data }); | |
| } else { | |
| // Typewise response ko format | |
| const componentsByType = data.reduce((acc, item) => { | |
| const type = item.type?.toUpperCase(); | |
| if (!type) return acc; | |
| acc[type] = item.components || []; | |
| return acc; | |
| }, {}); | |
| setComponentBy(componentsByType); | |
| } | |
| } | |
| } catch (error) { | |
| //console.log("error messages", error); | |
| } | |
| finally{ | |
| setLoadingComponents(false) | |
| } | |
| }; | |
| // runs on render/update of component list | |
| const redirectToPage=(e,pageId,pageNameCreate)=>{ | |
| e.stopPropagation(); | |
| useRoleStore.getState().setPageName(pageNameCreate) | |
| router.push(`/page?pageId=${pageId}`) | |
| } | |
| const redirectToEditDB=(e,schemaId)=>{ | |
| e.stopPropagation(); | |
| router.push(`/database?pageId=${schemaId}`) | |
| } | |
| const showDeletePageModal=(e,pageId,pageName)=>{ | |
| //console.log('pageName',pageName); | |
| e.stopPropagation(); | |
| setPageId(pageId); | |
| setPageName(pageName) | |
| setpageDeleteModal(true) | |
| } | |
| const updateComponentClick=(e)=>{ | |
| if(pageId && componentsPage.length>0){ | |
| updateSelectedPageById(e,pageId) | |
| } | |
| else if(pageId){ | |
| updatePageById(e,pageId) | |
| } | |
| } | |
| const redirectToDatabase=(e)=>{ | |
| e.stopPropagation() | |
| router.push('/create-table'); | |
| } | |
| const visitPublishWebsite=()=>{ | |
| router.push(`https://${subdomain}.${process.env.NEXT_PUBLIC_PUBLISH_URL}`) | |
| } | |
| // useLayoutEffect(() => { | |
| // components.forEach((comp) => { | |
| // const iframeId = comp.instanceId; | |
| // if (!iframeRefs.current[iframeId]) { | |
| // const iframe = document.createElement("iframe"); | |
| // iframe.style.width = "100%"; | |
| // iframe.style.minHeight = "50px"; | |
| // iframe.sandbox = "allow-scripts"; | |
| // iframe.src = comp.url; | |
| // iframeRefs.current[iframeId] = iframe; | |
| // } | |
| // const container = document.getElementById(`iframe-container-${iframeId}`); | |
| // const iframe = iframeRefs.current[iframeId]; | |
| // if (container && iframe.parentElement !== container) { | |
| // container.appendChild(iframe); | |
| // } | |
| // }); | |
| // }, [components]); | |
| // ✅ only when add/remove component | |
| // ✅ Only when component added/removed | |
| // ⚠️ only when component added/removed | |
| // ✅ empty dependency — no rerun on reorder | |
| // const MemoIframe = React.memo(({ instanceId, url, onLoad }) => { | |
| // const iframeRef = useRef(null); | |
| // useEffect(() => { | |
| // iframeRefs.current[instanceId] = iframeRef.current; | |
| // }, [instanceId]); | |
| // return ( | |
| // <iframe | |
| // ref={iframeRef} | |
| // id={`iframe-${instanceId}`} | |
| // src={url} | |
| // style={{ width: '100%', border: 'none', minHeight: '50px', height: '1px' }} | |
| // sandbox="allow-scripts" | |
| // onLoad={() => onLoad(instanceId)} | |
| // /> | |
| // ); | |
| // }, (prev, next) => prev.url === next.url && prev.instanceId === next.instanceId); | |
| if(isTooSmall && !manualContinueRef.current){ | |
| return( | |
| <div className={styles['container-screen']}> | |
| <div className={styles['header-screen']}> | |
| <h1 className={styles["screen-size-error-tite"]}>Screen Size Check</h1> | |
| </div> | |
| <div className={styles["warning-section"]}> | |
| <div className={styles["warning-icon"]}> | |
| <MonitorIcon className={`${styles.ph} ${styles["ph-monitor"]}`} /> | |
| </div> | |
| <div className={styles["warning-title"]}>Screen Size Not Supported</div> | |
| <div className={styles["warning-description"]}> | |
| Our platform requires a minimum screen size of 1200×700 pixels to work properly. | |
| Please switch to a larger device or expand your browser window. | |
| </div> | |
| </div> | |
| <div className={styles["requirements-section"]}> | |
| <div className={styles["section-header"]}> | |
| <div className={styles["section-icon"]}> | |
| <CheckCircleIcon className={`${styles.ph} ${styles["ph-check-circle"]}`} /> | |
| </div> | |
| <div className={styles["section-title"]}>Minimum Requirements</div> | |
| </div> | |
| <ul className={styles["requirements-list"]}> | |
| <li><MonitorIcon className={styles.ph}/> <strong>Screen Width:</strong> ≥ 1200</li> | |
| <li><MonitorIcon className={styles.ph}/> <strong>Screen Height:</strong> ≥ 700</li> | |
| <li><DesktopIcon className={styles.ph}/> <strong>Recommended:</strong> Desktop</li> | |
| </ul> | |
| </div> | |
| <div className={`${styles["screen-info-section"]} ${styles["bg-warning"]}`}> | |
| <div className={styles["screen-info-title"]}>Your Current Screen Size</div> | |
| <div className={styles["screen-dimensions"]}> | |
| {`${dimensions.width} X ${dimensions.height}`} | |
| </div> | |
| </div> | |
| <div className={styles["action-buttons"]} style={{ justifyContent: "center" }}> | |
| <button className={`${styles['btn-screen']} ${styles["btn-secondary-screen"]}`} onClick={handleContinue}> | |
| <WarningIcon className={styles.ph}/> Continue Anyway | |
| </button> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| const PageIframe = React.memo(({ instanceId, iframeSrcMap, pageIframeRefs, styles }) => { | |
| return ( | |
| <div id={`iframe-container-${instanceId}`} className={styles["component-content"]}> | |
| <iframe | |
| ref={(el) => { | |
| if (el) pageIframeRefs.current[instanceId] = el; | |
| else delete pageIframeRefs.current[instanceId]; | |
| }} | |
| src={iframeSrcMap.current[instanceId]} | |
| sandbox="allow-scripts" | |
| className={styles["component-iframe"]} | |
| style={{ border: "none", width: "100%", height: "1px", minHeight: "1px" }} | |
| onLoad={() => { | |
| //console.log("Iframe Loaded", instanceId); | |
| }} | |
| /> | |
| </div> | |
| ); | |
| }); | |
| return ( | |
| <> | |
| <div className={styles["main-content"]}> | |
| {toastModel && ( | |
| <div className={styles['toast-container']}> | |
| <div className={`${styles.toast} ${styles['success-light']} ${styles.show}`} id="toast-16"> | |
| <div className={styles["toast-icon"]}> | |
| <CheckCircleIcon className={` ${styles.ph} ${styles['ph-check-circle']}`}/> | |
| </div> | |
| <div className={styles["toast-content"]}> | |
| <div className={styles["toast-title"]}>Complete!</div> | |
| <div className={styles["toast-message"]}>Page cloned successfully.</div> | |
| </div> | |
| <button className={styles["toast-close"]} onClick={()=>setTotastModal(false)}> | |
| <XLogoIcon className={` ${styles.ph} ${styles['ph-x']}`}/> | |
| </button> | |
| <div className={styles["toast-progress"]}></div> | |
| </div> | |
| </div> | |
| )} | |
| {databaseToastModal && ( | |
| <div className={styles['toast-container']}> | |
| <div className={`${styles.toast} ${styles['success-light']} ${styles.show}`} id="toast-16"> | |
| <div className={styles["toast-icon"]}> | |
| <CheckCircleIcon className={` ${styles.ph} ${styles['ph-check-circle']}`}/> | |
| </div> | |
| <div className={styles["toast-content"]}> | |
| <div className={styles["toast-title"]}>Complete!</div> | |
| <div className={styles["toast-message"]}>Database cloned successfully.</div> | |
| </div> | |
| <button className={styles["toast-close"]} onClick={()=>setDatabaseToastModal(false)}> | |
| <XLogoIcon className={` ${styles.ph} ${styles['ph-x']}`}/> | |
| </button> | |
| <div className={styles["toast-progress"]}></div> | |
| </div> | |
| </div> | |
| )} | |
| {deletePageToast && ( | |
| <div className={styles['toast-container']}> | |
| <div className={`${styles.toast} ${styles['success-light']} ${styles.show}`} id="toast-16"> | |
| <div className={styles["toast-icon"]}> | |
| <CheckCircleIcon className={` ${styles.ph} ${styles['ph-check-circle']}`}/> | |
| </div> | |
| <div className={styles["toast-content"]}> | |
| <div className={styles["toast-title"]}>Complete!</div> | |
| <div className={styles["toast-message"]}>Page Deleted successfully.</div> | |
| </div> | |
| <button className={styles["toast-close"]} onClick={()=>setDeletePageToast(false)}> | |
| <XLogoIcon className={` ${styles.ph} ${styles['ph-x']}`}/> | |
| </button> | |
| <div className={styles["toast-progress"]}></div> | |
| </div> | |
| </div> | |
| )} | |
| {deleteDatabaseToast && ( | |
| <div className={styles['toast-container']}> | |
| <div className={`${styles.toast} ${styles['success-light']} ${styles.show}`} id="toast-16"> | |
| <div className={styles["toast-icon"]}> | |
| <CheckCircleIcon className={` ${styles.ph} ${styles['ph-check-circle']}`}/> | |
| </div> | |
| <div className={styles["toast-content"]}> | |
| <div className={styles["toast-title"]}>Complete!</div> | |
| <div className={styles["toast-message"]}>Database Archived successfully.</div> | |
| </div> | |
| <button className={styles["toast-close"]} onClick={()=>setDatabaseToastModal(false)}> | |
| <XLogoIcon className={` ${styles.ph} ${styles['ph-x']}`}/> | |
| </button> | |
| <div className={styles["toast-progress"]}></div> | |
| </div> | |
| </div> | |
| )} | |
| {publishModalError && ( | |
| <div className={styles['toast-container']}> | |
| <div className={`${styles.toast} ${styles['error-light']} ${styles.show}`} id="toast-16"> | |
| <div className={styles["toast-icon"]}> | |
| <CheckCircleIcon className={` ${styles.ph} ${styles['ph-check-circle']}`}/> | |
| </div> | |
| <div className={styles["toast-content"]}> | |
| <div className={styles["toast-title"]}>Error!</div> | |
| <div className={styles["toast-message"]}>Failed to Publish Website</div> | |
| </div> | |
| <button className={styles["toast-close"]} onClick={()=>setPublishModalError(false)}> | |
| <XLogoIcon className={` ${styles.ph} ${styles['ph-x']}`}/> | |
| </button> | |
| <div className={styles["toast-progress"]}></div> | |
| </div> | |
| </div> | |
| )} | |
| <div className={styles.navbar}> | |
| <div className={styles.logo}> | |
| <LightningIcon className={` ${styles.ph} ${styles['ph-lightning']}`}/> | |
| <span>Nocodevista</span> | |
| </div> | |
| <div className={styles["nav-buttons"]}> | |
| {publishButton && ( | |
| <button | |
| className={styles["btn-outline"]} | |
| onClick={() => handleGenerateZip(websiteId, pageId)} | |
| > | |
| <ClockCounterClockwiseIcon | |
| className={`${styles.ph} ${styles["ph-arrow-counter-clockwise"]}`} | |
| /> | |
| Publish | |
| </button> | |
| )} | |
| {saveText ? ( | |
| <button | |
| className={styles["btn-primary"]} | |
| onClick={(e) => { | |
| updateComponentClick(e); | |
| setPreviewButton(true); | |
| }} | |
| > | |
| <SpinnerIcon className={` ${styles.ph} ${styles["ph-floppy-disk"]}`} /> | |
| Saving | |
| </button> | |
| ) : (componentsData.length > 0 || componentsPage.length > 0) ? ( | |
| <button | |
| className={styles["btn-primary"]} | |
| onClick={(e) => { | |
| updateComponentClick(e); | |
| setPreviewButton(true); | |
| setPublishButton(true); | |
| }} | |
| > | |
| <FloppyDiskIcon className={` ${styles.ph} ${styles["ph-floppy-disk"]}`} /> | |
| Save and Preview | |
| </button> | |
| ) : null} | |
| </div> | |
| </div> | |
| <div className={styles.padding}> | |
| </div> | |
| <div className={styles['page-builder-canvas']}> | |
| <div className={styles["canvas-container"]}> | |
| <div className={styles.canvas}> | |
| {/* 🟢 1️⃣ If saved page components available */} | |
| { | |
| (WebsiteModal) ? ( | |
| <div className={styles["canvas-container"]}> | |
| <div className={styles.canvas}> | |
| <div className={styles["canvas-no-website"]}> | |
| <h3>No Website Selected</h3> | |
| <p>You need to select or create a website. Both options are available in the website management screen.</p> | |
| <div className={styles["no-website-actions"]}> | |
| <div className={styles["website-action-card"]} onClick={()=>{ | |
| redirectToWebsite(); | |
| }}> | |
| <div className={styles["website-action-card-icon"]}> | |
| <GlobeIcon className={` ${styles.ph} ${styles['ph-globe']}`}/> | |
| </div> | |
| <div className={styles["website-action-card-content"]}> | |
| <h4>Select Website</h4> | |
| <p>Select existing or create new website projects</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div className={styles["website-help-text"]}> | |
| <p><strong>💡 Quick Tip:</strong> Use the "Select Websites" option above to select an existing website or create a brand new one from scratch!</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ): | |
| fetchedPages === null ? null : | |
| (fetchedPages.length<=0) ? ( | |
| <div className={styles["canvas-container"]}> | |
| <div className={styles['canvas-empty']}> | |
| <div className={styles["no-page-placeholder"]}> | |
| <div className={styles["no-page-icon"]}> | |
| <FilePlusIcon className={` ${styles.ph} ${styles['ph-file-plus']}`}/> | |
| </div> | |
| <h3>No pages created yet</h3> | |
| <p>Create your first page to start building your website. You can add components, customize layouts, and bring your vision to life.</p> | |
| <div className={styles["no-page-actions"]}> | |
| <button className={styles["create-page-btn"]} onClick={()=>redirectPageCreation()}> | |
| <PlusIcon className={` ${styles.ph} ${styles['ph-plus']}`}/> | |
| Create New Page | |
| </button> | |
| </div> | |
| <div className={styles["no-page-tip"]}> | |
| <LightningIcon className={` ${styles.ph} ${styles['ph-lightbulb']}`}/> | |
| <span> | |
| <strong>Quick tip:</strong> Start with a homepage template or create a blank page. You can always add more pages like About, Contact, or Services later. | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ):(!pageId) ? ( | |
| <div className={styles["canvas-container"]}> | |
| <div className={styles.canvas}> | |
| <div className={styles["canvas-select-page"]}> | |
| <h3>Select a Page to Edit</h3> | |
| <p>Choose an existing page from your left menu to start building, or create a new page to begin your design journey.</p> | |
| <div className={styles["select-page-actions"]}> | |
| <div className={styles["action-card"]} onClick={() => { | |
| if (typeof window.onPanelOpen === "function") { | |
| window.onPanelOpen("page"); // 🔹 panel khulega | |
| } else { | |
| //console.warn("❌ onPanelOpen not available"); | |
| } | |
| }}> | |
| <div className={styles["action-card-icon"]}> | |
| <FilesIcon className={` ${styles.ph} ${styles['ph-files']}`}/> | |
| </div> | |
| <div className={styles["action-card-content"]}> | |
| <h4>Select Existing Page</h4> | |
| <p>Choose from Home, Products, About, or Contact pages</p> | |
| </div> | |
| </div> | |
| <div className={styles["action-card"]} onClick={()=>{ | |
| redirectPageCreation() | |
| }}> | |
| <div className={styles["action-card-icon"]}> | |
| <PlusCircleIcon className={` ${styles.ph} ${styles['ph-plus-circle']}`}/> | |
| </div> | |
| <div className={styles["action-card-content"]}> | |
| <h4>Create New Page</h4> | |
| <p>Start fresh with a blank page or template</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ):(sortedComponentsPage.length > 0) ? ( | |
| sortedComponentsPage.map((comp, index) => { | |
| const canMoveUp = index > 0; | |
| const canMoveDown = index < sortedComponentsPage.length - 1; | |
| return ( | |
| <div | |
| key={comp.instanceId} | |
| className={`${loadingStatesPage[comp.instanceId] === false ? styles["canvas-component"] : ""}`} | |
| > | |
| {/* Move Up / Down Controls */} | |
| {loadingStatesPage[comp.instanceId] === false && movingPageId!==comp.instanceId && ( | |
| <div className={styles["move-controls"]}> | |
| {canMoveUp && ( | |
| <button | |
| className={`${styles['move-btn']} ${styles['move-up-btn']}`} | |
| title="Move component up" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| movePageComponentWithFeedback(index, index - 1, comp.instanceId); | |
| }} | |
| > | |
| <CaretUpIcon className={`${styles.ph} ${styles['ph-caret-up']}`} /> | |
| </button> | |
| )} | |
| {canMoveDown && ( | |
| <button | |
| className={`${styles['move-btn']} ${styles['move-down-btn']}`} | |
| title="Move component down" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| movePageComponentWithFeedback(index, index + 1, comp.instanceId); | |
| }} | |
| > | |
| <CaretDownIcon className={`${styles.ph} ${styles['ph-caret-down']}`} /> | |
| </button> | |
| )} | |
| </div> | |
| )} | |
| {/* TOP add button */} | |
| {loadingStatesPage[comp.instanceId] === false && movingPageId!==comp.instanceId && ( | |
| <div className={styles["add-btn-wrapper"]}> | |
| <button | |
| className={styles["add-component-top"]} | |
| onClick={() => { | |
| setShowModel(true); | |
| setPositionForNewComponent(`top:${comp.instanceId}`); | |
| }} | |
| > | |
| + | |
| </button> | |
| </div> | |
| )} | |
| {/* Delete btn */} | |
| {loadingStatesPage[comp.instanceId] === false && movingPageId!==comp.instanceId && ( | |
| <button | |
| className={styles["delete-component-btn"]} | |
| onClick={() => { | |
| deletePageComponent(comp.instanceId); | |
| }} | |
| > | |
| <TrashIcon /> | |
| </button> | |
| )} | |
| {/* {(loadingStatesPage[comp.instanceId] === false && movingPageId!==comp.instanceId) && ( | |
| <button | |
| className={styles["properties-component-btn"]} | |
| > | |
| <GearSixIcon/> | |
| </button> | |
| )} */} | |
| {/* Content block (loader + iframe) */} | |
| {movingPageId===comp.instanceId ? ( | |
| <div className={styles["moving-container"]} style={{ minHeight: "50px" }}> | |
| <div className={styles.spinner}></div> | |
| <p>Loading content, please wait...</p> | |
| </div> | |
| ):( | |
| <div | |
| id={`iframe-container-${comp.instanceId}`} | |
| className={styles["component-content"]} | |
| > | |
| {/* Loader */} | |
| {loadingStatesPage[comp.instanceId] !== false && ( | |
| <div | |
| className={styles["loader-container"]} | |
| style={{ minHeight: "200px" }} | |
| > | |
| <div className={styles.spinner}></div> | |
| <p>Loading content, please wait...</p> | |
| </div> | |
| )} | |
| {/* Iframe */} | |
| <iframe | |
| ref={(el) => { | |
| pageIframeRefs.current[comp.instanceId] = el; | |
| //console.log(`🔹 Iframe ref set for ${comp.instanceId}:`, el); | |
| //console.log(`📥 Iframe onLoad fired for ${comp.instanceId}`); | |
| //console.log('comp file Path',comp.filePath) | |
| }} | |
| src={comp.filePath} | |
| sandbox="allow-scripts" | |
| className={styles["component-iframe"]} | |
| style={{ | |
| border: "none", | |
| width: "100%", | |
| display: loadingStatesPage[comp.instanceId] !== false ? "none" : "block", | |
| height: loadingStatesPage[comp.instanceId] === false ? "1px" : "200px", | |
| minHeight: loadingStatesPage[comp.instanceId] === false ? "1px" : "200px", | |
| }} | |
| onLoad={() => { | |
| //console.log(`Iframe src for ${comp.instanceId}:`, comp.filePath); | |
| handleIframeLoadPage(comp.instanceId); | |
| }} | |
| /> | |
| </div> | |
| )} | |
| {/* BOTTOM add button */} | |
| {loadingStatesPage[comp.instanceId] === false && movingPageId!==comp.instanceId && ( | |
| <div className={styles["add-btn-wrapper"]}> | |
| <button | |
| className={styles["add-component-bottom"]} | |
| onClick={() => { | |
| setShowModel(true); | |
| setPositionForNewComponent(`bottom:${comp.instanceId}`); | |
| }} | |
| > | |
| + | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }) | |
| // 🟢 2️⃣ If both are empty | |
| ) : | |
| (componentsData.length>0) ? ( | |
| componentsData.map((comp, index) => { | |
| const canMoveUp = index > 0; | |
| const canMoveDown = index < componentsData.length - 1; | |
| return ( | |
| <div | |
| key={`${comp.instanceId}-${comp.order}`} | |
| id={`component-${comp.instanceId}`} | |
| className={styles["canvas-component"]} | |
| style={{ width: "100%", position: "relative", order: comp.order }} | |
| > | |
| {/* Move Up/Down Controls */} | |
| {loadingStates[comp.instanceId]===false && movingId!==comp.instanceId && ( | |
| <div className={styles["move-controls"]}> | |
| {canMoveUp && ( | |
| <button | |
| className={` ${styles["move-btn"]} ${styles['move-up-btn']}`} | |
| title="Move component up" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| moveComponentWithFeedback(index, index - 1, comp.instanceId); | |
| }} | |
| > | |
| <CaretUpIcon className={` ${styles.ph} ${styles['ph-caret-up']}`}/> | |
| </button> | |
| )} | |
| {canMoveDown && ( | |
| <button | |
| className={` ${styles['move-btn']} ${styles['move-down-btn']}`} | |
| title="Move component down" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| moveComponentWithFeedback(index, index + 1, comp.instanceId); | |
| }} | |
| > | |
| <CaretDownIcon className={` ${styles.ph} ${styles['ph-caret-down']}`}/> | |
| </button> | |
| )} | |
| </div> | |
| )} | |
| {/* Add Component Above */} | |
| {(loadingStates[comp.instanceId]===false && movingId!==comp.instanceId) && ( | |
| <button | |
| className={styles["add-component-top"]} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| setShowModel(true); | |
| setPositionForNewComponent(`top:${comp.instanceId}`); | |
| }} | |
| > | |
| + | |
| </button> | |
| )} | |
| {/* Delete Component */} | |
| {(loadingStates[comp.instanceId]===false && movingId!==comp.instanceId) && ( | |
| <button | |
| className={styles["delete-component-btn"]} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| deleteComponent(comp.instanceId); | |
| }} | |
| > | |
| <TrashIcon /> | |
| </button> | |
| )} | |
| {/* {(loadingStates[comp.instanceId] === false && movingId!==comp.instanceId) && ( | |
| <button | |
| className={styles["properties-component-btn"]} | |
| onClick={() => { | |
| setSliderToggle(true); | |
| setSliders('sliders'); | |
| setSelectedComponentInstanceId(comp.instanceId); | |
| }} | |
| > | |
| <GearSixIcon/> | |
| </button> | |
| )} */} | |
| {/* Component Iframe */} | |
| {movingId===comp.instanceId ? ( | |
| <div className={styles["moving-container"]} style={{ minHeight: "50px" }}> | |
| <div className={styles.spinner}></div> | |
| <p>Loading content, please wait...</p> | |
| </div> | |
| ):( | |
| <div | |
| id={`iframe-container-${comp.instanceId}`} | |
| className={styles["component-content"]} | |
| style={{ position: "relative"}} | |
| > | |
| {/* Loader */} | |
| {loadingStates[comp.instanceId] !== false && ( | |
| <div className={styles["loader-container"]} style={{ minHeight: "200px" }}> | |
| <div className={styles.spinner}></div> | |
| <p>Loading content, please wait...</p> | |
| </div> | |
| )} | |
| {/* Iframe always mounted */} | |
| <iframe | |
| ref={(el) => { | |
| if (el) pageIframeRefs.current[comp.instanceId] = el; | |
| }} | |
| id={`iframe-${comp.instanceId}`} | |
| src={comp.filePath} | |
| style={{ | |
| width: "100%", | |
| border: "none", | |
| display: loadingStates[comp.instanceId] !== false ? "none" : "block", // toggle visibility only | |
| height:loadingStates[comp.instanceId]!==false?"200px":"1px", | |
| minHeight:loadingStates[comp.instanceId]!==false?"200px":"50px" | |
| }} | |
| sandbox="allow-scripts" | |
| onLoad={() => { | |
| //console.log('iframe loaded',comp.filePath) | |
| handleIframeLoad(comp.instanceId) | |
| }} | |
| /> | |
| </div> | |
| )} | |
| {/* Add Component Below */} | |
| {(loadingStates[comp.instanceId]===false && movingId!==comp.instanceId) && ( | |
| <button | |
| className={styles["add-component-bottom"]} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| setShowModel(true); | |
| setPositionForNewComponent(`bottom:${comp.instanceId}`); | |
| }} | |
| > | |
| + | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| }) | |
| ):components.length === 0 && componentsPage.length === 0 && ( | |
| <div className={styles["canvas-placeholder"]}> | |
| <button | |
| className={styles["add-component-btn"]} | |
| onClick={() => { | |
| setShowModel(true); | |
| setPositionForNewComponent("empty"); | |
| }} | |
| > | |
| <PlusIcon /> | |
| </button> | |
| <h3>Start Building Your Page</h3> | |
| <p>Click the plus button to add components</p> | |
| </div> | |
| // 🟢 3️⃣ If local components added (before save) | |
| ) | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {slidertoggle && selectedLinkProps && manuallyEditedLinks && ( | |
| <div | |
| className={`${styles["properties-panel"]} ${ | |
| sliders === "sliders" ? styles.open : "" | |
| }`} | |
| ref={panelRef} | |
| id="propertiesPanel" | |
| > | |
| <div className={styles["properties-header"]}> | |
| <div className={styles["properties-title"]}> | |
| <SlidersIcon | |
| className={`${styles.ph} ${styles["ph-sliders"]}`} | |
| /> | |
| Properties | |
| </div> | |
| <button | |
| className={styles["close-properties"]} | |
| onClick={() =>{ | |
| setSliderToggle(false); | |
| setSelectedComponentInstanceId(null); | |
| }} | |
| > | |
| <XIcon className={`${styles.ph} ${styles["ph-x"]}`} /> | |
| </button> | |
| </div> | |
| <div className={styles["properties-content"]}> | |
| {selectedLinkProps | |
| ?.filter(link => link.instanceId === selectedComponentInstanceId) | |
| .map((link, idx) => ( | |
| <div key={idx} className={styles["property-group"]}> | |
| <div className={styles["property-group-title"]}> | |
| Label #{ | |
| typeof link.text === "string" | |
| ? link.text | |
| .replace(/<style[^>]*>[\s\S]*?<\/style>/g, "") | |
| .replace(/<[^>]*>/g, "") | |
| .trim() | |
| : Array.isArray(link.text) | |
| ? link.text.join(" ").trim() | |
| : (link.text?.innerText || link.text?.text || "").toString() | |
| } | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>URL</label> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={ | |
| linkInputCache[`${link.instanceId}-${link.key}`] !== undefined | |
| ? linkInputCache[`${link.instanceId}-${link.key}`] | |
| : link.href | |
| } | |
| onChange={(e) => { | |
| const newHref = e.target.value; | |
| const idKey = `${link.instanceId}-${link.key}`; | |
| setManuallyEditedLinks((prev) => ({ | |
| ...prev, | |
| [idKey]: { | |
| ...(prev[idKey] || {}), | |
| href: newHref, | |
| }, | |
| })); | |
| const overrideKey = `aHref${link.key}`; | |
| const overrideValue = newHref; | |
| // Update componentsData | |
| setComponentsData((prev) => | |
| prev.map((comp) => | |
| comp.instanceId === link.instanceId | |
| ? { | |
| ...comp, | |
| overrides: { | |
| ...comp.overrides, | |
| [overrideKey]: overrideValue, | |
| }, | |
| } | |
| : comp | |
| ) | |
| ); | |
| // Update componentsPage also | |
| if (componentsPage.length > 0) { | |
| setComponentsPage((prev) => | |
| prev.map((comp) => | |
| comp.instanceId === link.instanceId | |
| ? { | |
| ...comp, | |
| overrides: { | |
| ...comp.overrides, | |
| [overrideKey]: overrideValue, | |
| }, | |
| } | |
| : comp | |
| ) | |
| ); | |
| } | |
| const pageIframe = | |
| pageIframeRefs.current[link.instanceId]; | |
| pageIframe?.contentWindow?.postMessage( | |
| { | |
| type: "UPDATE_COMPONENT_DATA", | |
| source: "parent", | |
| instanceId: link.instanceId, | |
| overrides: { [overrideKey]: overrideValue }, | |
| mode: "local", | |
| }, | |
| "*" | |
| ); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| ))} | |
| {ActiveSection === "text" && !windowReady && ( | |
| <div className={styles.accordion}> | |
| {/* Formatting */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "formatting" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("formatting")} | |
| data-accordion="formatting" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextAaIcon className={`${styles.ph} ${styles["ph-text-aa"]}`} /> | |
| <span>Formatting</span> | |
| </div> | |
| {/* Rotating caret */} | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "formatting" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "formatting" ? styles["active"] : "" | |
| }`} | |
| id="formatting" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["button-group"]}> | |
| {[ | |
| { key: "bold", icon: <TextBIcon />, title: "Bold (Ctrl+B)" }, | |
| { key: "italic", icon: <TextItalicIcon />, title: "Italic (Ctrl+I)" }, | |
| { key: "underline", icon: <TextUnderlineIcon />, title: "Underline (Ctrl+U)" }, | |
| { key: "strike", icon: <TextStrikethroughIcon />, title: "Strikethrough" }, | |
| { key: "superscript", icon: <TextSuperscriptIcon />, title: "Superscript" }, | |
| { key: "subscript", icon: <TextSubscriptIcon />, title: "Subscript" }, | |
| ].map(({ key, icon, title }) => ( | |
| <button | |
| key={key} | |
| className={`${styles["format-button"]} ${ | |
| activeFormats[key] ? styles["active"] : "" | |
| }`} | |
| data-command={key} | |
| title={title} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| applyFormattingText(key, true); | |
| }} | |
| > | |
| {icon} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Font & Size */} | |
| <div className={styles["accordion-section"]}> | |
| {/* Accordion Header */} | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "font" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("font")} | |
| data-accordion="font" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextTIcon className={`${styles.ph} ${styles["ph-text-t"]}`} /> | |
| <span>Font & Size</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "font" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| {/* Accordion Content */} | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "font" ? styles["active"] : "" | |
| }`} | |
| id="font" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* Font Family */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Font Family</label> | |
| <select | |
| className={styles["property-select"]} | |
| value={propertyValues.fontFamily || "inherit"} // fallback safe | |
| onChange={(e) => { | |
| const newVal = e.target.value; | |
| setPropertyValues((prev) => ({ ...prev, fontFamily: newVal })); | |
| applyFormatting("fontFamily", newVal); | |
| }} | |
| > | |
| <option value="inherit">Default (Inter)</option> | |
| <optgroup label="⭐ Web Safe Fonts"> | |
| <option value="Arial">Arial</option> | |
| <option value="Georgia">Georgia</option> | |
| <option value="Times New Roman">Times New Roman</option> | |
| <option value="Courier New">Courier New</option> | |
| </optgroup> | |
| </select> | |
| </div> | |
| {/* Font Size */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Font Size (px)</label> | |
| <div className={styles["number-input-group"]}> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => handleFontSizeChange(fontSize - 1)} | |
| > | |
| <MinusIcon className={`${styles.ph} ${styles["ph-minus"]}`} /> | |
| </button> | |
| <input | |
| type="number" | |
| className={styles["property-input"]} | |
| value={fontSize} | |
| min="8" | |
| max="120" | |
| onChange={(e) => | |
| handleFontSizeChange(parseInt(e.target.value, 10)) | |
| } | |
| /> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => handleFontSizeChange(fontSize + 1)} | |
| > | |
| <PlusIcon className={`${styles.ph} ${styles["ph-plus"]}`} /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Colors */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "colors" ? styles["active"] : "" | |
| }`} | |
| onClick={() => { | |
| toggleAccordion("colors"); | |
| console.log("colors accordion clicked", activeAccordion); | |
| }} | |
| data-accordion="colors" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <PaletteIcon className={`${styles.ph} ${styles["ph-palette"]}`} /> | |
| <span>Colors</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "colors" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "colors" ? styles["active"] : "" | |
| }`} | |
| id="colors" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* 🎨 Text Color */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Text Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| onChange={(e) => { | |
| const color = e.target.value.trim(); | |
| applyFormatting("color", color); | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={propertyValues.color} | |
| onChange={(e) => { | |
| const color = e.target.value.trim(); | |
| setPropertyValues((prev) => ({ ...prev, color })); | |
| }} | |
| onBlur={(e) => { | |
| let color = e.target.value.trim(); | |
| // 🎨 Convert rgb(255, 255, 255) → #ffffff | |
| if (color.startsWith("rgb")) { | |
| const match = color.match(/\d+/g); | |
| if (match) { | |
| const [r, g, b] = match.map(Number); | |
| color = `#${((1 << 24) + (r << 16) + (g << 8) + b) | |
| .toString(16) | |
| .slice(1) | |
| .toUpperCase()}`; | |
| } | |
| } | |
| if (/^#[0-9A-Fa-f]{6}$/.test(color)) { | |
| applyFormatting("color", color); | |
| } else { | |
| console.warn("⚠️ Invalid color format:", color); | |
| } | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* 🎨 Background Color */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Background Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| onChange={(e) => { | |
| const bgColor = e.target.value.trim(); | |
| applyFormatting("backgroundColor", bgColor); | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={propertyValues.backgroundColor} | |
| onChange={(e) => { | |
| const bgColor = e.target.value.trim(); | |
| setPropertyValues((prev) => ({ ...prev, backgroundColor: bgColor })); | |
| }} | |
| onBlur={(e) => { | |
| let bgColor = e.target.value.trim(); | |
| // 🎨 Convert rgb(255, 255, 255) → #FFFFFF | |
| if (bgColor.startsWith("rgb")) { | |
| const match = bgColor.match(/\d+/g); | |
| if (match) { | |
| const [r, g, b] = match.map(Number); | |
| bgColor = `#${((1 << 24) + (r << 16) + (g << 8) + b) | |
| .toString(16) | |
| .slice(1) | |
| .toUpperCase()}`; | |
| } | |
| } | |
| // ✅ Validate and apply | |
| if (/^#[0-9A-Fa-f]{6}$/.test(bgColor)) { | |
| applyFormatting("backgroundColor", bgColor); | |
| } else { | |
| console.warn("⚠️ Invalid background color format:", bgColor); | |
| } | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Spacing */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "spacing" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("spacing")} | |
| data-accordion="spacing" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextAlignJustifyIcon className={` ${styles.ph} ${styles['ph-text-align-justify']}`}/> | |
| <span>Spacing</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "spacing" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "spacing" ? styles["active"] : "" | |
| }`} | |
| id="spacing" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Line Height</label> | |
| <span className={styles["slider-value"]}>{lineHeightValue}</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0.5" | |
| max="3" | |
| step="0.1" | |
| value={lineHeightValue} | |
| onChange={(e) => { | |
| setLineHeightValue(e.target.value); | |
| applyFormatting("lineHeight", e.target.value); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Letter Spacing (px)</label> | |
| <span className={styles["slider-value"]}>{letterSpacingValue}</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="-5" | |
| max="20" | |
| step="0.5" | |
| value={letterSpacingValue} | |
| onChange={(e) => { | |
| setLetterSpacingValue(e.target.value); | |
| applyFormatting("letterSpacing", e.target.value + "px"); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Link */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "link" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("link")} | |
| data-accordion="link" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <LinkIcon className={`${styles.ph} ${styles['ph-link']}`}/> | |
| <span>Link</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "link" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "link" ? styles["active"] : "" | |
| }`} | |
| id="link" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Link URL</label> | |
| <input | |
| type="url" | |
| className={styles["property-input"]} | |
| placeholder="https://example.com" | |
| /> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label | |
| className={styles["property-label"]} | |
| style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" }} | |
| > | |
| <input type="checkbox" style={{ width: "auto" }} defaultChecked /> | |
| Open in new tab | |
| </label> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Link Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| defaultValue="#1F3A65" | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| defaultValue="#1F3A65" | |
| /> | |
| </div> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Text Decoration</label> | |
| <select className={styles["property-select"]} defaultValue="underline"> | |
| <option value="underline">Underline</option> | |
| <option value="none">None</option> | |
| <option value="overline">Overline</option> | |
| <option value="line-through">Line Through</option> | |
| </select> | |
| </div> | |
| <div | |
| className={styles["button-group"]} | |
| style={{ marginTop: "16px", display: "flex", gap: "8px" }} | |
| > | |
| <button | |
| className={styles["property-button-primary"]} | |
| style={{ flex: 1 }} | |
| onClick={() => { | |
| const url = document.querySelector(`#link input[type="url"]`)?.value.trim() || "#"; | |
| const openInNewTab = document.querySelector(`#link input[type="checkbox"]`)?.checked; | |
| const color = document.querySelector(`#link input[type="color"]`)?.value; | |
| const textDecoration = document.querySelector(`#link select`)?.value; | |
| const target = openInNewTab ? "_blank" : "_self"; | |
| const linkData = { url, target, color, textDecoration }; | |
| applyFormatting("link", linkData); | |
| }} | |
| > | |
| <CheckIcon className={` ${styles.ph} ${styles['ph-check']}`}/> | |
| Apply Link | |
| </button> | |
| <button | |
| className={styles["property-button"]} | |
| style={{ flex: 1 }} | |
| onClick={() => { | |
| applyFormatting("removeLink", true); | |
| }} | |
| > | |
| <LinkBreakIcon className={` ${styles.ph} ${styles['ph-link-break']}`}/> | |
| Remove | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "show-link" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("show-link")} | |
| data-accordion="show-link" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <LinkIcon className={`${styles.ph} ${styles['ph-link']}`}/> | |
| <span>Show Link</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "show-link" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "show-link" ? styles["active"] : "" | |
| }`} | |
| id="link" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["property-section"]} id="emptyState"> | |
| <div className={styles["property-header"]}> | |
| <h3 className={styles["property-title"]}> | |
| <LinkSimpleIcon className={` ${styles.ph} ${styles['ph-link']}`}/> | |
| All Page Links | |
| </h3> | |
| <div className={styles["property-badge"]}>${selectedLinkProps.length} link${selectedLinkProps.length !== 1 ? 's' : ''}</div> | |
| </div> | |
| </div> | |
| <div className={styles["property-section"]} id="emptyState"> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}> | |
| <MagnifyingGlassIcon className={`ph ph-magnifying-glass ${styles.ph}`}/> | |
| Search Links | |
| </label> | |
| <input type="text" className={styles["property-input"]} id="searchLinksInput" placeholder="Search by text or URL..."/> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}> | |
| <FunnelIcon className={`ph ph-funnel ${styles.ph}`}/> | |
| Filter by Type | |
| </label> | |
| <select className={styles["property-select"]} id="filterLinkType"> | |
| <option value="all">All Links (${selectedLinkProps.length})</option> | |
| <option value="internal">Internal Links</option> | |
| <option value="external">External Links</option> | |
| <option value="email">Email Links</option> | |
| <option value="tel">Phone Links</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div id="linksListContainer" className={styles.linksListContainer}> | |
| {selectedLinkProps.map((link) => { | |
| const isExternal = isExternalLink(link.href); | |
| return ( | |
| <div | |
| key={`${link.instanceId}-${link.key}`} // ✅ KEY ADDED | |
| className={styles["link-item"]} | |
| data-link-id={`${link.instanceId}-${link.key}`} | |
| > | |
| <div className={styles["link-item-header"]}> | |
| <div | |
| className={styles["link-item-icon"]} | |
| style={{ | |
| background: isExternal ? "#3498DB" : "#10B981", | |
| }} | |
| > | |
| <LinkIcon | |
| className={isExternal ? "ph ph-arrow-square-out" : "ph ph-link"} | |
| /> | |
| </div> | |
| <div className={styles["link-item-content"]}> | |
| <div className={styles["link-item-text"]}> | |
| {getCleanLinkLabel(link.text, "No text")} | |
| </div> | |
| <div className={styles["link-item-url"]}> | |
| {truncateUrl(link.href)} | |
| </div> | |
| </div> | |
| {isExternal && ( | |
| <span className={styles["link-badge"]}>NEW TAB</span> | |
| )} | |
| </div> | |
| <div className={styles["link-item-actions"]}> | |
| <button | |
| className={styles["link-action-btn"]} | |
| onClick={() => { | |
| setSelectedLinkId(link); | |
| //setSliders(''); | |
| setWindowReady(true); | |
| }} | |
| > | |
| <PencilIcon className={` ${styles.ph} ${styles["ph-pencil"]}`} /> | |
| </button> | |
| <button | |
| className={styles["link-action-btn"]} | |
| onClick={() => onNavigateToLink(link)} | |
| > | |
| <CursorIcon | |
| className={` ${styles.ph} ${styles["ph-cursor-click"]}`} | |
| /> | |
| </button> | |
| <button | |
| className={ | |
| styles["link-action-btn"] + " " + styles["link-action-delete"] | |
| } | |
| onClick={() => onDeleteLink(link)} | |
| > | |
| <TrashIcon className={` ${styles.ph} ${styles["ph-trash"]}`} /> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div id="linksListContainer" className={styles.linksListContainer}> | |
| {selectedLinkId && ( | |
| <> | |
| <div className={styles["property-header"]}> | |
| <button | |
| className={styles["property-button"]} | |
| id="backToListBtn" | |
| style={{ padding: "8px 12px", marginBottom: "12px" }} | |
| onClick={() => setWindowReady(false)} | |
| > | |
| <ArrowLeftIcon className={`${styles.ph} ${styles["ph-arrow-left"]}`} /> | |
| Back to List | |
| </button> | |
| <h3 className={styles["property-title"]}> | |
| <LinkIcon className={`${styles.ph} ${styles["ph-link"]}`} /> | |
| </h3> | |
| </div> | |
| <div style={{ padding: "4px 0" }}> | |
| {selectedLinkProps | |
| .filter(link => | |
| link.instanceId === selectedLinkId.instanceId && | |
| link.key === selectedLinkId.key | |
| ) | |
| .map((link) => { | |
| const idKey = `${link.instanceId}-${link.key}`; | |
| const textOverrideKey = `aText${link.key}`; | |
| const hrefOverrideKey = `aHref${link.key}`; | |
| return ( | |
| <div key={idKey}> | |
| {/* Link Text */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Link Text</label> | |
| {/* Link Text */} | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={ | |
| linkInputCache[`${idKey}-text`] !== undefined | |
| ? linkInputCache[`${idKey}-text`] | |
| : link.text ?? "" | |
| } | |
| onChange={(e) => { | |
| const newText = e.target.value; | |
| // Update text cache | |
| setLinkInputCache(prev => ({ | |
| ...prev, | |
| [`${idKey}-text`]: newText, | |
| })); | |
| // manualEditedLinks | |
| setManuallyEditedLinks(prev => ({ | |
| ...prev, | |
| [idKey]: { ...(prev[idKey] || {}), text: newText }, | |
| })); | |
| // componentsData | |
| setComponentsData(prev => | |
| prev.map(comp => | |
| comp.instanceId === link.instanceId | |
| ? { ...comp, overrides: { ...comp.overrides, [`aText${link.key}`]: newText } } | |
| : comp | |
| ) | |
| ); | |
| // componentsPage | |
| if (componentsPage?.length) { | |
| setComponentsPage(prev => | |
| prev.map(comp => | |
| comp.instanceId === link.instanceId | |
| ? { ...comp, overrides: { ...comp.overrides, [`aText${link.key}`]: newText } } | |
| : comp | |
| ) | |
| ); | |
| } | |
| }} | |
| /> | |
| </div> | |
| {/* Link URL */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Link URL</label> | |
| <input | |
| type="url" | |
| className={styles["property-input"]} | |
| value={ | |
| linkInputCache[idKey] !== undefined | |
| ? linkInputCache[idKey] | |
| : link.href ?? "" | |
| } | |
| onChange={(e) => { | |
| const newHref = e.target.value; | |
| // 🟢 1. Update local cache (MOST IMPORTANT) | |
| setLinkInputCache((prev) => ({ | |
| ...prev, | |
| [idKey]: newHref, | |
| })); | |
| // 🟢 2. Update manualEditedLinks | |
| setManuallyEditedLinks((prev) => ({ | |
| ...prev, | |
| [idKey]: { ...(prev[idKey] || {}), href: newHref }, | |
| })); | |
| // 🟢 3. Update componentsData | |
| setComponentsData((prev) => | |
| prev.map((comp) => | |
| comp.instanceId === link.instanceId | |
| ? { | |
| ...comp, | |
| overrides: { | |
| ...comp.overrides, | |
| [`aHref${link.key}`]: newHref, | |
| }, | |
| } | |
| : comp | |
| ) | |
| ); | |
| // 🟢 4. Update componentsPage | |
| if (componentsPage?.length) { | |
| setComponentsPage((prev) => | |
| prev.map((comp) => | |
| comp.instanceId === link.instanceId | |
| ? { | |
| ...comp, | |
| overrides: { | |
| ...comp.overrides, | |
| [`aHref${link.key}`]: newHref, | |
| }, | |
| } | |
| : comp | |
| ) | |
| ); | |
| } | |
| }} | |
| /> | |
| </div> | |
| {/* Open in new tab */} | |
| <div className={styles["property-field"]}> | |
| <label | |
| className={styles["property-label"]} | |
| style={{ display: "flex", alignItems: "center", gap: 8 }} | |
| > | |
| <input | |
| type="checkbox" | |
| checked={link.target === "_blank"} | |
| onChange={(e) => { | |
| const newTarget = e.target.checked ? "_blank" : "_self"; | |
| setManuallyEditedLinks(prev => ({ | |
| ...prev, | |
| [idKey]: { ...(prev[idKey] || {}), target: newTarget }, | |
| })); | |
| }} | |
| /> | |
| Open in new tab | |
| </label> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {/* Buttons */} | |
| <div className={styles["button-group"]}> | |
| <button | |
| className={styles["property-button"]} | |
| onClick={() => setWindowReady(false)} | |
| > | |
| Cancel | |
| </button> | |
| <button className={styles["property-button-primary"]} onClick={handleSaveSelectedLink}> | |
| Save | |
| </button> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| {ActiveSection === "image" && ( | |
| <> | |
| {/* 🖼 Image Info */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "image-info" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("image-info")} | |
| data-accordion="image-info" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <InfoIcon className={`${styles.ph} ${styles["ph-info"]}`} /> | |
| <span>Image Info</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "image-info" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "image-info" ? styles["active"] : "" | |
| }`} | |
| id="image-info" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* Alt Text */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}> | |
| Alt Text (for accessibility) | |
| </label> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| id="imageAltText" | |
| value={altText} | |
| onChange={(e) => setAltText(e.target.value)} | |
| placeholder="Describe the image" | |
| /> | |
| </div> | |
| {/* Title */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Title (optional)</label> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| id="imageTitle" | |
| value={imageTitle} | |
| onChange={(e) => setImageTitle(e.target.value)} | |
| placeholder="Image title" | |
| /> | |
| </div> | |
| {/* Save Info Button */} | |
| <button | |
| className={styles["property-button-primary"]} | |
| id="saveImageAlt" | |
| style={{ marginTop: "8px" }} | |
| onClick={(e)=>{ | |
| e.stopPropagation(); | |
| handleSaveImageMeta(); | |
| }} // ✅ Added React onClick | |
| > | |
| <CheckIcon className={styles.ph} /> | |
| <span>Save Info</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 🔄 Replace Image */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${activeAccordion === "image-replace" ? styles["active"] : ""}`} | |
| onClick={() => toggleAccordion("image-replace")} | |
| data-accordion="image-replace" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <SwapIcon className={`${styles.ph} ${styles["ph-swap"]}`} /> | |
| <span>Replace Image</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${activeAccordion === "image-replace" ? styles["rotated"] : ""}`} | |
| /> | |
| </button> | |
| <div className={`${styles["accordion-content"]} ${activeAccordion === "image-replace" ? styles["active"] : ""}`} id="image-replace"> | |
| <div className={styles["accordion-body"]}> | |
| {/* Upload Area */} | |
| {!uploading && ( | |
| <div | |
| className={styles["upload-area"]} | |
| onClick={() => fileInputRef.current?.click()} | |
| > | |
| <div className={styles["upload-icon"]}><CloudUploadIcon /></div> | |
| <div className={styles["upload-text"]}>Drop image here</div> | |
| <div className={styles["upload-hint"]}>or click to browse</div> | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| className={styles["file-input"]} | |
| accept="image/*" | |
| onChange={handleFileChange} | |
| style={{ display: "none" }} | |
| /> | |
| </div> | |
| )} | |
| {/* Progress Bar */} | |
| {uploading && ( | |
| <div style={{ marginTop: "12px" }}> | |
| <div style={{ background: "#e5e7eb", borderRadius: "4px", height: "8px", overflow: "hidden" }}> | |
| <div | |
| style={{ | |
| background: progress === 100 ? "linear-gradient(90deg, #44D7B6, #3bc4a8)" : "#3bc4a8", | |
| height: "100%", | |
| width: `${progress}%`, | |
| transition: "width 0.3s" | |
| }} | |
| ></div> | |
| </div> | |
| <div className={`${styles["text-muted"]} ${styles['text-center']} ${styles['mt-16']}`}> | |
| {progress < 100 ? `Uploading... ${progress}%` : "Upload complete!"} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* ✨ Image Effects */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "image-effects" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("image-effects")} | |
| data-accordion="image-effects" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <MagicWandIcon className={`${styles.ph} ${styles["ph-magic-wand"]}`} /> | |
| <span>Effects</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "image-effects" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "image-effects" ? styles["active"] : "" | |
| }`} | |
| id="image-effects" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* Opacity */} | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Opacity</label> | |
| <span className={styles["slider-value"]}> | |
| {imageOpacity}% | |
| </span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0" | |
| max="100" | |
| step="5" | |
| value={imageOpacity} | |
| onChange={(e) => { | |
| const value = Number(e.target.value); | |
| setImageOpacity(value); | |
| sendImageEffectEvent(value, imageBlur); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Blur */} | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Blur</label> | |
| <span className={styles["slider-value"]}> | |
| {imageBlur}px | |
| </span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0" | |
| max="30" | |
| step="2" | |
| value={imageBlur} | |
| onChange={(e) => { | |
| const value = Number(e.target.value); | |
| setImageBlur(value); | |
| sendImageEffectEvent(imageOpacity, value); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| {ActiveSection === "video" && ( | |
| <> | |
| {/* 🎬 Video Info */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${styles.active}`} | |
| data-accordion="video-info" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <InfoIcon className={`${styles.ph} ${styles["ph-info"]}`} /> | |
| <span>Video Info</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${styles["accordion-icon"]}`} | |
| /> | |
| </button> | |
| <div className={`${styles["accordion-content"]} ${styles.active}`} id="video-info"> | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Video Source</label> | |
| <div | |
| style={{ | |
| background: "#f8f9fa", | |
| padding: "12px", | |
| borderRadius: "6px", | |
| textAlign: "center", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| fontSize: "14px", | |
| fontWeight: 600, | |
| color: "#1F3A65", | |
| textTransform: "uppercase", | |
| }} | |
| id="videoSourceBadge" | |
| > | |
| YouTube | |
| </div> | |
| <div | |
| style={{ | |
| fontSize: "12px", | |
| color: "#6b7280", | |
| marginTop: "4px", | |
| }} | |
| > | |
| Current Source | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 🔁 Replace Video */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "video-replace" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("video-replace")} | |
| data-accordion="video-replace" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <SwapIcon className={`${styles.ph} ${styles["ph-swap"]}`} /> | |
| <span>Replace Video</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "video-replace" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "video-replace" ? styles["active"] : "" | |
| }`} | |
| id="video-replace" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* Property Tabs */} | |
| <div className={styles["property-tabs"]}> | |
| <button | |
| className={`${styles["property-tab"]} ${ | |
| activeVideoTab === "url" ? styles["active"] : "" | |
| }`} | |
| onClick={() => setActiveVideoTab("url")} | |
| > | |
| <LinkIcon className={`${styles.ph} ${styles["ph-link"]}`} /> | |
| Video URL | |
| </button> | |
| <button | |
| className={`${styles["property-tab"]} ${ | |
| activeVideoTab === "upload" ? styles["active"] : "" | |
| }`} | |
| onClick={() => setActiveVideoTab("upload")} | |
| > | |
| <UploadIcon className={`${styles.ph} ${styles["ph-upload"]}`} /> | |
| Upload | |
| </button> | |
| </div> | |
| {/* 🌐 URL Tab */} | |
| {activeVideoTab === "url" && ( | |
| <div className={`${styles["tab-content"]} ${styles.active}`} id="url-tab"> | |
| <div className={`${styles["property-field"]}`}> | |
| <label className={styles["property-label"]}>Video URL</label> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| placeholder="Paste video URL here..." | |
| value={videoUrl} | |
| onChange={(e) => setVideoUrl(e.target.value)} | |
| /> | |
| <div className={`${styles["text-muted"]} ${styles["mt-16"]}`}> | |
| <strong>Supports:</strong> <br /> | |
| • YouTube: https://youtube.com/watch?v=... <br /> | |
| • Vimeo: https://vimeo.com/... <br /> | |
| • Direct: https://example.com/video.mp4 | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| className={`${styles["property-button-primary"]} ${styles["mt-16"]}`} | |
| onClick={(e)=>{ | |
| handleApplyVideoUrl(e) | |
| }} | |
| > | |
| <CheckIcon className={`${styles.ph} ${styles["ph-check"]}`} /> | |
| Apply Video URL | |
| </button> | |
| </div> | |
| )} | |
| {/* ⬆️ Upload Tab */} | |
| {activeVideoTab === "upload" && ( | |
| <div className={`${styles["tab-content"]} ${styles.active}`} id="upload-tab"> | |
| <div className={styles["accordion-body"]}> | |
| {/* Upload Area */} | |
| {!uploading && ( | |
| <div | |
| className={styles["upload-area"]} | |
| onClick={() => videoFileInputRef.current?.click()} | |
| > | |
| <div className={styles["upload-icon"]}><CloudUploadIcon /></div> | |
| <div className={styles["upload-text"]}>Drop video here</div> | |
| <div className={styles["upload-hint"]}>or click to browse</div> | |
| <input | |
| type="file" | |
| ref={videoFileInputRef} | |
| className={styles["file-input"]} | |
| accept="video/*" | |
| onChange={(e)=>{ | |
| console.log('e printed'); | |
| e.stopPropagation(); | |
| handleVideoChange(e);}} | |
| style={{ display: "none" }} | |
| /> | |
| </div> | |
| )} | |
| {/* Progress Bar */} | |
| {uploading && ( | |
| <div style={{ marginTop: "12px" }}> | |
| <div style={{ background: "#e5e7eb", borderRadius: "4px", height: "8px", overflow: "hidden" }}> | |
| <div | |
| style={{ | |
| background: progress === 100 ? "linear-gradient(90deg, #44D7B6, #3bc4a8)" : "#3bc4a8", | |
| height: "100%", | |
| width: `${progress}%`, | |
| transition: "width 0.3s" | |
| }} | |
| ></div> | |
| </div> | |
| <div className={`${styles["text-muted"]} ${styles['text-center']} ${styles['mt-16']}`}> | |
| {progress < 100 ? `Uploading... ${progress}%` : "Upload complete!"} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* ⚙️ Video Settings */} | |
| {/* <div className={styles["accordion-section"]}> | |
| <button | |
| type="button" | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "video-settings" ? styles["active"] : "" | |
| }`} | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| toggleAccordion("video-settings"); | |
| }} | |
| data-accordion="video-settings" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <SettingsIcon className={`${styles.ph} ${styles["ph-gear"]}`} /> | |
| <span>Settings</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "video-settings" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "video-settings" ? styles["active"] : "" | |
| }`} | |
| id="video-settings" | |
| style={{ | |
| overflow: activeAccordion === "video-settings" ? "visible" : "hidden", | |
| pointerEvents: activeAccordion === "video-settings" ? "auto" : "none", | |
| }} | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {[ | |
| { | |
| id: "videoAutoplay", | |
| label: "Autoplay", | |
| value: videoAutoplay, | |
| setter: setVideoAutoplay, | |
| }, | |
| { | |
| id: "videoMuted", | |
| label: "Muted", | |
| value: videoMuted, | |
| setter: setVideoMuted, | |
| }, | |
| { | |
| id: "videoLoop", | |
| label: "Loop", | |
| value: videoLoop, | |
| setter: setVideoLoop, | |
| }, | |
| { | |
| id: "videoControls", | |
| label: "Show Controls", | |
| value: videoControls, | |
| setter: setVideoControls, | |
| }, | |
| ].map((item) => ( | |
| <div key={item.id} className={styles["property-field"]}> | |
| <label | |
| htmlFor={item.id} | |
| className={styles["property-label"]} | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "8px", | |
| cursor: "pointer", | |
| }} | |
| > | |
| <input | |
| type="checkbox" | |
| id={item.id} | |
| checked={item.value} | |
| onChange={(e) => item.setter(e.target.checked)} | |
| style={{ width: "auto" }} | |
| /> | |
| {item.label} | |
| </label> | |
| </div> | |
| ))} | |
| <div className={`${styles["text-muted"]} ${styles["mt-16"]}`}> | |
| Note: Autoplay with sound may be blocked by browsers. | |
| </div> | |
| <button | |
| type="button" | |
| className={`${styles["property-button-primary"]} ${styles["mt-16"]}`} | |
| id="applyVideoSettings" | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| console.log("✅ Button Clicked — Message Printed in Videos"); | |
| console.log("🎥 Applying video settings", { | |
| autoplay: videoAutoplay, | |
| muted: videoMuted, | |
| loop: videoLoop, | |
| controls: videoControls, | |
| }); | |
| if (!selectedComponentInstanceId) { | |
| console.warn("⚠️ No selected instanceId found"); | |
| return; | |
| } | |
| const targetIframe = | |
| pageIframeRefs.current?.[selectedComponentInstanceId]; | |
| if (!targetIframe?.contentWindow) { | |
| console.warn("⚠️ No iframe found for selected instanceId"); | |
| return; | |
| } | |
| targetIframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_VIDEO_SETTINGS", | |
| instanceId: selectedComponentInstanceId, | |
| payload: { | |
| autoplay: videoAutoplay, | |
| muted: videoMuted, | |
| loop: videoLoop, | |
| controls: videoControls, | |
| }, | |
| }, | |
| "*" | |
| ); | |
| console.log("✅ Sent UPDATE_VIDEO_SETTINGS to iframe"); | |
| }} | |
| > | |
| <CheckIcon className={`${styles.ph} ${styles["ph-check"]}`} /> | |
| Apply Settings | |
| </button> | |
| </div> | |
| </div> | |
| </div> */} | |
| </> | |
| )} | |
| {ActiveSection === "background" && ( | |
| <> | |
| {/* 🔄 Replace Background Image */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${activeAccordion === "bg-image-replace" ? styles["active"] : ""}`} | |
| onClick={() => toggleAccordion("bg-image-replace")} | |
| data-accordion="bg-image-replace" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <SwapIcon className={`${styles.ph} ${styles["ph-swap"]}`} /> | |
| <span>Replace Background Image</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${activeAccordion === "bg-image-replace" ? styles["rotated"] : ""}`} | |
| /> | |
| </button> | |
| <div className={`${styles["accordion-content"]} ${activeAccordion === "bg-image-replace" ? styles["active"] : ""}`} id="bg-image-replace"> | |
| <div className={styles["accordion-body"]}> | |
| {!uploadingBg && ( | |
| <div | |
| className={styles["upload-area"]} | |
| onClick={() => fileInputBgRef.current?.click()} | |
| > | |
| <div className={styles["upload-icon"]}><CloudUploadIcon /></div> | |
| <div className={styles["upload-text"]}>Drop image here</div> | |
| <div className={styles["upload-hint"]}>or click to browse</div> | |
| <input | |
| type="file" | |
| ref={fileInputBgRef} | |
| className={styles["file-input"]} | |
| accept="image/*" | |
| onChange={(e)=>{ | |
| handleBgFileChange(e) | |
| }} | |
| style={{ display: "none" }} | |
| /> | |
| </div> | |
| )} | |
| {uploadingBg && ( | |
| <div style={{ marginTop: "12px" }}> | |
| <div style={{ background: "#e5e7eb", borderRadius: "4px", height: "8px", overflow: "hidden" }}> | |
| <div | |
| style={{ | |
| background: "#3bc4a8", | |
| height: "100%", | |
| width: `${progress}%`, | |
| transition: "width 0.3s" | |
| }} | |
| ></div> | |
| </div> | |
| <div className={`${styles["text-muted"]} ${styles['text-center']} ${styles['mt-16']}`}> | |
| {progress < 100 ? `Uploading... ${progress}%` : "Upload complete!"} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* ✨ Background Image Effects */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${activeAccordion === "bg-image-effects" ? styles["active"] : ""}`} | |
| onClick={() => toggleAccordion("bg-image-effects")} | |
| data-accordion="bg-image-effects" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <MagicWandIcon className={`${styles.ph} ${styles["ph-magic-wand"]}`} /> | |
| <span>Effects</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${activeAccordion === "bg-image-effects" ? styles["rotated"] : ""}`} | |
| /> | |
| </button> | |
| <div className={`${styles["accordion-content"]} ${activeAccordion === "bg-image-effects" ? styles["active"] : ""}`} id="bg-image-effects"> | |
| <div className={styles["accordion-body"]}> | |
| {/* Opacity */} | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Opacity</label> | |
| <span className={styles["slider-value"]}>{bgImageOpacity}%</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0" | |
| max="100" | |
| step="5" | |
| value={bgImageOpacity} | |
| onChange={(e) => { | |
| const value = Number(e.target.value); | |
| setBgImageOpacity(value); | |
| sendBgImageEffectEvent(value, bgImageBlur); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Blur */} | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Blur</label> | |
| <span className={styles["slider-value"]}>{bgImageBlur}px</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0" | |
| max="30" | |
| step="2" | |
| value={bgImageBlur} | |
| onChange={(e) => { | |
| const value = Number(e.target.value); | |
| setBgImageBlur(value); | |
| sendBgImageEffectEvent(bgImageOpacity, value); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className={styles.accordion}> | |
| {/* Formatting */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "formatting" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("formatting")} | |
| data-accordion="formatting" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextAaIcon className={`${styles.ph} ${styles["ph-text-aa"]}`} /> | |
| <span>Formatting</span> | |
| </div> | |
| {/* Rotating caret */} | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "formatting" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "formatting" ? styles["active"] : "" | |
| }`} | |
| id="formatting" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["button-group"]}> | |
| {[ | |
| { key: "bold", icon: <TextBIcon />, title: "Bold (Ctrl+B)" }, | |
| { key: "italic", icon: <TextItalicIcon />, title: "Italic (Ctrl+I)" }, | |
| { key: "underline", icon: <TextUnderlineIcon />, title: "Underline (Ctrl+U)" }, | |
| { key: "strike", icon: <TextStrikethroughIcon />, title: "Strikethrough" }, | |
| { key: "superscript", icon: <TextSuperscriptIcon />, title: "Superscript" }, | |
| { key: "subscript", icon: <TextSubscriptIcon />, title: "Subscript" }, | |
| ].map(({ key, icon, title }) => ( | |
| <button | |
| key={key} | |
| className={`${styles["format-button"]} ${ | |
| activeFormats[key] ? styles["active"] : "" | |
| }`} | |
| data-command={key} | |
| title={title} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| applyFormattingText(key, true); | |
| }} | |
| > | |
| {icon} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Font & Size */} | |
| <div className={styles["accordion-section"]}> | |
| {/* Accordion Header */} | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "font" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("font")} | |
| data-accordion="font" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextTIcon className={`${styles.ph} ${styles["ph-text-t"]}`} /> | |
| <span>Font & Size</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "font" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| {/* Accordion Content */} | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "font" ? styles["active"] : "" | |
| }`} | |
| id="font" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* Font Family */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Font Family</label> | |
| <select | |
| className={styles["property-select"]} | |
| value={propertyValues.fontFamily || "inherit"} // fallback safe | |
| onChange={(e) => { | |
| const newVal = e.target.value; | |
| setPropertyValues((prev) => ({ ...prev, fontFamily: newVal })); | |
| applyFormatting("fontFamily", newVal); | |
| }} | |
| > | |
| <option value="inherit">Default (Inter)</option> | |
| <optgroup label="⭐ Web Safe Fonts"> | |
| <option value="Arial">Arial</option> | |
| <option value="Helvetica">Helvetica</option> | |
| <option value="Georgia">Georgia</option> | |
| <option value="Times New Roman">Times New Roman</option> | |
| <option value="Courier New">Courier New</option> | |
| </optgroup> | |
| </select> | |
| </div> | |
| {/* Font Size */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Font Size (px)</label> | |
| <div className={styles["number-input-group"]}> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => handleFontSizeChange(fontSize - 1)} | |
| > | |
| <MinusIcon className={`${styles.ph} ${styles["ph-minus"]}`} /> | |
| </button> | |
| <input | |
| type="number" | |
| className={styles["property-input"]} | |
| value={fontSize} | |
| min="8" | |
| max="120" | |
| onChange={(e) => | |
| handleFontSizeChange(parseInt(e.target.value, 10)) | |
| } | |
| /> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => handleFontSizeChange(fontSize + 1)} | |
| > | |
| <PlusIcon className={`${styles.ph} ${styles["ph-plus"]}`} /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Colors */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "colors" ? styles["active"] : "" | |
| }`} | |
| onClick={() => { | |
| toggleAccordion("colors"); | |
| console.log("colors accordion clicked", activeAccordion); | |
| }} | |
| data-accordion="colors" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <PaletteIcon className={`${styles.ph} ${styles["ph-palette"]}`} /> | |
| <span>Colors</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "colors" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "colors" ? styles["active"] : "" | |
| }`} | |
| id="colors" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* 🎨 Text Color */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Text Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| onChange={(e) => { | |
| const color = e.target.value.trim(); | |
| applyFormatting("color", color); | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={propertyValues.color} | |
| onChange={(e) => { | |
| const color = e.target.value.trim(); | |
| setPropertyValues((prev) => ({ ...prev, color })); | |
| }} | |
| onBlur={(e) => { | |
| let color = e.target.value.trim(); | |
| // 🎨 Convert rgb(255, 255, 255) → #ffffff | |
| if (color.startsWith("rgb")) { | |
| const match = color.match(/\d+/g); | |
| if (match) { | |
| const [r, g, b] = match.map(Number); | |
| color = `#${((1 << 24) + (r << 16) + (g << 8) + b) | |
| .toString(16) | |
| .slice(1) | |
| .toUpperCase()}`; | |
| } | |
| } | |
| if (/^#[0-9A-Fa-f]{6}$/.test(color)) { | |
| applyFormatting("color", color); | |
| } else { | |
| console.warn("⚠️ Invalid color format:", color); | |
| } | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* 🎨 Background Color */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Background Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| onChange={(e) => { | |
| const bgColor = e.target.value.trim(); | |
| applyFormatting("backgroundColor", bgColor); | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={propertyValues.backgroundColor} | |
| onChange={(e) => { | |
| const bgColor = e.target.value.trim(); | |
| setPropertyValues((prev) => ({ ...prev, backgroundColor: bgColor })); | |
| }} | |
| onBlur={(e) => { | |
| let bgColor = e.target.value.trim(); | |
| // 🎨 Convert rgb(255, 255, 255) → #FFFFFF | |
| if (bgColor.startsWith("rgb")) { | |
| const match = bgColor.match(/\d+/g); | |
| if (match) { | |
| const [r, g, b] = match.map(Number); | |
| bgColor = `#${((1 << 24) + (r << 16) + (g << 8) + b) | |
| .toString(16) | |
| .slice(1) | |
| .toUpperCase()}`; | |
| } | |
| } | |
| // ✅ Validate and apply | |
| if (/^#[0-9A-Fa-f]{6}$/.test(bgColor)) { | |
| applyFormatting("backgroundColor", bgColor); | |
| } else { | |
| console.warn("⚠️ Invalid background color format:", bgColor); | |
| } | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Spacing */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "spacing" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("spacing")} | |
| data-accordion="spacing" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextAlignJustifyIcon className={` ${styles.ph} ${styles['ph-text-align-justify']}`}/> | |
| <span>Spacing</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "spacing" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "spacing" ? styles["active"] : "" | |
| }`} | |
| id="spacing" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Line Height</label> | |
| <span className={styles["slider-value"]}>{lineHeightValue}</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0.5" | |
| max="3" | |
| step="0.1" | |
| value={lineHeightValue} | |
| onChange={(e) => { | |
| setLineHeightValue(e.target.value); | |
| applyFormatting("lineHeight", e.target.value); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Letter Spacing (px)</label> | |
| <span className={styles["slider-value"]}>{letterSpacingValue}</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="-5" | |
| max="20" | |
| step="0.5" | |
| value={letterSpacingValue} | |
| onChange={(e) => { | |
| setLetterSpacingValue(e.target.value); | |
| applyFormatting("letterSpacing", e.target.value + "px"); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Link */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "link" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("link")} | |
| data-accordion="link" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <LinkIcon className={`${styles.ph} ${styles['ph-link']}`}/> | |
| <span>Link</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "link" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "link" ? styles["active"] : "" | |
| }`} | |
| id="link" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Link URL</label> | |
| <input | |
| type="url" | |
| className={styles["property-input"]} | |
| placeholder="https://example.com" | |
| /> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label | |
| className={styles["property-label"]} | |
| style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" }} | |
| > | |
| <input type="checkbox" style={{ width: "auto" }} defaultChecked /> | |
| Open in new tab | |
| </label> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Link Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| defaultValue="#1F3A65" | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| defaultValue="#1F3A65" | |
| /> | |
| </div> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Text Decoration</label> | |
| <select className={styles["property-select"]} defaultValue="underline"> | |
| <option value="underline">Underline</option> | |
| <option value="none">None</option> | |
| <option value="overline">Overline</option> | |
| <option value="line-through">Line Through</option> | |
| </select> | |
| </div> | |
| <div | |
| className={styles["button-group"]} | |
| style={{ marginTop: "16px", display: "flex", gap: "8px" }} | |
| > | |
| <button | |
| className={styles["property-button-primary"]} | |
| style={{ flex: 1 }} | |
| onClick={() => { | |
| const url = document.querySelector(`#link input[type="url"]`)?.value.trim() || "#"; | |
| const openInNewTab = document.querySelector(`#link input[type="checkbox"]`)?.checked; | |
| const color = document.querySelector(`#link input[type="color"]`)?.value; | |
| const textDecoration = document.querySelector(`#link select`)?.value; | |
| const target = openInNewTab ? "_blank" : "_self"; | |
| const linkData = { url, target, color, textDecoration }; | |
| applyFormatting("link", linkData); | |
| }} | |
| > | |
| <CheckIcon className={` ${styles.ph} ${styles['ph-check']}`}/> | |
| Apply Link | |
| </button> | |
| <button | |
| className={styles["property-button"]} | |
| style={{ flex: 1 }} | |
| onClick={() => { | |
| applyFormatting("removeLink", true); | |
| }} | |
| > | |
| <LinkBreakIcon className={` ${styles.ph} ${styles['ph-link-break']}`}/> | |
| Remove | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| {ActiveSection === "background-style" && ( | |
| <> | |
| {/* 🎨 Background Type Selector */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "bg-type-selector" ? styles.active : "" | |
| }`} | |
| onClick={() => toggleAccordion("bg-type-selector")} | |
| data-accordion="bg-type-selector" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <PaletteIcon className={`${styles.ph} ${styles["ph-palette"]}`} /> | |
| <span>Background Style</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "bg-type-selector" ? styles.rotated : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "bg-type-selector" ? styles.active : "" | |
| }`} | |
| id="bg-type-selector" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <label className={styles["property-label"]}> | |
| Choose Background Type | |
| </label> | |
| <select className={styles["bg-type-dropdown"]} id="bgTypeDropdown"> | |
| <option value="solid">🎨 Solid Color</option> | |
| <option value="gradient">🌈 Gradient</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 🟦 Solid Color Options */} | |
| <div | |
| className={`${styles["bg-options-panel"]} ${ | |
| activeBgType === "solid" ? styles.active : "" | |
| }`} | |
| id="solidOptions" | |
| > | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "solid-controls" ? styles.active : "" | |
| }`} | |
| onClick={() => toggleAccordion("solid-controls")} | |
| data-accordion="solid-controls" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <PaletteIcon className={`${styles.ph} ${styles["ph-palette"]}`} /> | |
| <span>Color Options</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "solid-controls" ? styles.rotated : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "solid-controls" ? styles.active : "" | |
| }`} | |
| id="solid-controls" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["preset-colors-large"]}> | |
| {[ | |
| "#1F3A65", | |
| "#6366F1", | |
| "#8B5CF6", | |
| "#EC4899", | |
| "#EF4444", | |
| "#F59E0B", | |
| "#10B981", | |
| "#00BFA6", | |
| "#3B82F6", | |
| "#0D1B2A", | |
| "#FFFFFF", | |
| "#F3F4F6", | |
| ].map((color, index) => ( | |
| <div | |
| key={index} | |
| className={styles["preset-color-large"]} | |
| style={{ | |
| background: color, | |
| border: | |
| color === "#FFFFFF" ? "2px solid #E5E7EB" : "none", | |
| }} | |
| data-color={color} | |
| title={color} | |
| /> | |
| ))} | |
| </div> | |
| <details style={{ marginTop: "16px" }}> | |
| <summary | |
| style={{ | |
| cursor: "pointer", | |
| fontSize: "13px", | |
| fontWeight: 500, | |
| color: "#6b7280", | |
| padding: "8px 0", | |
| }} | |
| > | |
| + Custom Color | |
| </summary> | |
| <div style={{ paddingTop: "12px" }}> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| id="bgColor" | |
| defaultValue="#1F3A65" | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| id="bgColorHex" | |
| defaultValue="#1F3A65" | |
| placeholder="#000000" | |
| style={{ flex: 1 }} | |
| /> | |
| </div> | |
| </div> | |
| </details> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 🌈 Gradient Options */} | |
| <div | |
| className={`${styles["bg-options-panel"]} ${ | |
| activeBgType === "gradient" ? styles.active : "" | |
| }`} | |
| id="gradientOptions" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <label className={styles["property-label"]}>Gradient Type</label> | |
| <div className={styles["gradient-type-pills-simple"]}> | |
| {["linear", "radial", "conic", "multi", "animated"].map((type) => ( | |
| <button | |
| key={type} | |
| className={`${styles["gradient-pill-simple"]} ${ | |
| selectedGradientType === type ? styles.active : "" | |
| }`} | |
| onClick={() => setSelectedGradientType(type)} | |
| > | |
| <span className={styles["pill-icon"]}> | |
| {type === "linear" | |
| ? "↗" | |
| : type === "radial" | |
| ? "◉" | |
| : type === "conic" | |
| ? "◐" | |
| : type === "multi" | |
| ? "▦" | |
| : "⚡"} | |
| </span> | |
| <span>{type.charAt(0).toUpperCase() + type.slice(1)}</span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Custom Gradient Color Controls */} | |
| <div className={styles["accordion-body"]}> | |
| <label className={styles["property-label"]}>Custom Colors</label> | |
| {[1, 2, 3].map((num) => ( | |
| <div | |
| key={num} | |
| id={`gradientColor${num}Container`} | |
| style={{ display: num === 3 && !showThirdColor ? "none" : "block" }} | |
| > | |
| <label | |
| className={styles["property-label"]} | |
| style={{ fontSize: "12px", marginBottom: "6px" }} | |
| > | |
| {`Color ${num}${num === 3 ? " (Middle)" : ""}`} | |
| </label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| id={`gradientColor${num}`} | |
| defaultValue={num === 1 ? "#1F3A65" : num === 2 ? "#2a5087" : "#764ba2"} | |
| style={{ width: "40px" }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| id={`gradientColor${num}Hex`} | |
| defaultValue={ | |
| num === 1 ? "#1F3A65" : num === 2 ? "#2a5087" : "#764ba2" | |
| } | |
| style={{ flex: 1 }} | |
| /> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Gradient Angle + Animation Speed */} | |
| <div className={styles["accordion-body"]}> | |
| <label className={styles["property-label"]}>Direction / Angle</label> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| id="gradientAngle" | |
| min="0" | |
| max="360" | |
| step="15" | |
| defaultValue="135" | |
| /> | |
| <div style={{ textAlign: "center", marginTop: "6px" }}> | |
| <span id="gradientAngleValue" style={{ fontWeight: 600, color: "#1F3A65" }}> | |
| 135° | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| {/* ☁️ Shadow Options */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "shadow-options" ? styles.active : "" | |
| }`} | |
| onClick={() => toggleAccordion("shadow-options")} | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <DropHalfBottomIcon className={`${styles.ph} ${styles["ph-drop-half-bottom"]}`} /> | |
| <span>Shadow</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "shadow-options" ? styles.rotated : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "shadow-options" ? styles.active : "" | |
| }`} | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div | |
| style={{ | |
| display: "flex", | |
| justifyContent: "space-between", | |
| marginBottom: "16px", | |
| padding: "12px", | |
| background: "#F9FAFB", | |
| borderRadius: "8px", | |
| }} | |
| > | |
| <label className={styles["property-label"]}>Enable Shadow</label> | |
| <label className={styles["shadow-toggle"]}> | |
| <input type="checkbox" id="shadowEnable" /> | |
| <span className={styles["toggle-slider"]}></span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 🔄 Reset */} | |
| <div | |
| style={{ | |
| padding: "20px", | |
| margin: "0 -20px -20px -20px", | |
| borderTop: "1px solid rgba(0,0,0,0.06)", | |
| background: "#fafbfc", | |
| }} | |
| > | |
| <button | |
| className={styles["property-button-primary"]} | |
| id="resetBackgroundBtn" | |
| style={{ width: "100%" }} | |
| > | |
| <ArrowCounterClockwiseIcon className={`${styles.ph} ${styles["ph-arrow-counter-clockwise"]}`} /> | |
| Reset to Original | |
| </button> | |
| </div> | |
| </> | |
| )} | |
| {ActiveSection === "custome-fields" && ( | |
| <> | |
| {/* 🟦 Manage Items Section */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${styles.active}`} | |
| data-accordion="container-manage" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <SquaresFourIcon | |
| className={`${styles.ph} ${styles["ph-squares-four"]}`} | |
| /> | |
| <span>Manage Items</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${styles["accordion-icon"]}`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${styles.active}`} | |
| id="container-manage" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div | |
| style={{ | |
| background: "#F0F9FF", | |
| border: "1px solid #BAE6FD", | |
| borderRadius: "8px", | |
| padding: "12px", | |
| marginBottom: "16px", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "8px", | |
| marginBottom: "4px", | |
| }} | |
| > | |
| <InfoIcon | |
| className={`${styles.ph} ${styles["ph-info"]}`} | |
| style={{ color: "#0284C7", fontSize: "16px" }} | |
| /> | |
| <strong style={{ color: "#0369A1", fontSize: "13px" }}> | |
| Container Selected | |
| </strong> | |
| </div> | |
| <p | |
| style={{ | |
| margin: 0, | |
| fontSize: "12px", | |
| color: "#075985", | |
| lineHeight: "1.5", | |
| }} | |
| > | |
| You can add or remove items (cards/divs) from this container. | |
| </p> | |
| </div> | |
| <div | |
| style={{ display: "flex", gap: "8px", marginBottom: "16px" }} | |
| > | |
| <button | |
| className="property-button-primary" | |
| id="addContainerItem" | |
| style={{ flex: 1 }} | |
| > | |
| <PlusCircleIcon | |
| className={`${styles.ph} ${styles["ph-plus-circle"]}`} | |
| /> | |
| Add Item | |
| </button> | |
| <button | |
| className="property-button" | |
| id="removeContainerItem" | |
| style={{ | |
| flex: 1, | |
| background: "#FEE2E2", | |
| color: "#DC2626", | |
| border: "1px solid #FECACA", | |
| }} | |
| > | |
| <TrashIcon | |
| className={`${styles.ph} ${styles["ph-trash"]}`} | |
| /> | |
| Remove Last | |
| </button> | |
| </div> | |
| <div | |
| id="containerItemCount" | |
| style={{ | |
| textAlign: "center", | |
| padding: "12px", | |
| background: "#F9FAFB", | |
| borderRadius: "6px", | |
| fontSize: "13px", | |
| color: "#6B7280", | |
| }} | |
| > | |
| <strong style={{ color: "#111827" }}>0</strong> items in container | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 🟨 Layout Settings Section */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${styles.active}`} | |
| data-accordion="container-layout" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <LayoutIcon className={`${styles.ph} ${styles["ph-layout"]}`} /> | |
| <span>Layout Settings</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${styles["accordion-icon"]}`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${styles.active}`} | |
| id="container-layout" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <label className="property-label">Display Mode</label> | |
| <div | |
| className="property-tabs" | |
| style={{ marginTop: "8px", marginBottom: "16px" }} | |
| > | |
| <button | |
| className="property-tab active" | |
| id="gridModeBtn" | |
| data-mode="grid" | |
| > | |
| <SquaresFourIcon className={`${styles.ph} ${styles["ph-squares-four"]}`} /> | |
| Grid | |
| </button> | |
| <button | |
| className="property-tab" | |
| id="carouselModeBtn" | |
| data-mode="carousel" | |
| > | |
| <ArrowsHorizontalIcon className={`${styles.ph} ${styles["ph-arrows-horizontal"]}`} /> | |
| Carousel | |
| </button> | |
| </div> | |
| {/* Grid Options */} | |
| <div id="gridOptions" style={{ display: "block" }}> | |
| <label className="property-label">Columns</label> | |
| <select | |
| className="bg-type-dropdown" | |
| id="containerColumns" | |
| style={{ marginTop: "8px" }} | |
| ></select> | |
| </div> | |
| {/* Carousel Options */} | |
| <div id="carouselOptions" style={{ display: "none" }}> | |
| <label className="property-label">Items Per Slide</label> | |
| <select | |
| className="bg-type-dropdown" | |
| id="itemsPerSlide" | |
| style={{ marginTop: "8px" }} | |
| > | |
| <option value="1">1 Item</option> | |
| <option value="2">2 Items</option> | |
| <option value="3" selected> | |
| 3 Items | |
| </option> | |
| <option value="4">4 Items</option> | |
| </select> | |
| <label | |
| className="property-label" | |
| style={{ marginTop: "16px" }} | |
| > | |
| Auto-play | |
| </label> | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| gap: "12px", | |
| marginTop: "8px", | |
| }} | |
| > | |
| <label className="shadow-toggle"> | |
| <input type="checkbox" id="carouselAutoplay" /> | |
| <span className="toggle-slider"></span> | |
| </label> | |
| <span style={{ fontSize: "13px", color: "#6B7280" }}> | |
| Enable auto-scroll | |
| </span> | |
| </div> | |
| <div | |
| id="autoplaySpeedControl" | |
| style={{ display: "none", marginTop: "16px" }} | |
| > | |
| <label className="property-label">Auto-play Speed</label> | |
| <input | |
| type="range" | |
| id="autoplaySpeed" | |
| min="2" | |
| max="10" | |
| defaultValue="4" | |
| style={{ width: "100%", marginTop: "8px" }} | |
| /> | |
| <div | |
| style={{ | |
| textAlign: "center", | |
| marginTop: "4px", | |
| fontSize: "13px", | |
| color: "#6B7280", | |
| }} | |
| > | |
| <span id="speedValue">4</span> seconds | |
| </div> | |
| </div> | |
| </div> | |
| {/* Gap Between Items */} | |
| <label | |
| className="property-label" | |
| style={{ marginTop: "16px" }} | |
| > | |
| Gap Between Items | |
| </label> | |
| <input | |
| type="range" | |
| id="containerGap" | |
| min="0" | |
| max="60" | |
| defaultValue="24" | |
| style={{ width: "100%", marginTop: "8px" }} | |
| /> | |
| <div | |
| style={{ | |
| textAlign: "center", | |
| marginTop: "4px", | |
| fontSize: "13px", | |
| color: "#6B7280", | |
| }} | |
| > | |
| <span id="gapValue">24</span>px | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| {/* 🎨 ICON PROPERTIES PANEL */} | |
| {ActiveSection === "icon" && ( | |
| <div className={styles["icon-properties-panel"]}> | |
| {/* ===== ICON INFO ===== */} | |
| <div className={styles["property-group"]}> | |
| <div className={styles["property-group-title"]}> | |
| <InfoIcon className={`${styles.ph} ${styles["ph-info"]}`} /> | |
| <span>Icon Info</span> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <div className={styles["icon-preview-box"]}> | |
| <div | |
| className={styles["icon-preview"]} | |
| style={{ | |
| color: currentColor, | |
| fontSize: currentSize, | |
| transform: ` | |
| rotate(${currentRotation}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| }} | |
| > | |
| {(() => { | |
| const library = selectedIcon?.library; | |
| const name = selectedIcon?.name; | |
| console.log("selected icon library", library); | |
| console.log("name Printed", name); | |
| if (!library || !name) return <span>Icon not found</span>; | |
| // ----------------------------------- | |
| // 🔥 ICONIFY FALLBACK | |
| // ----------------------------------- | |
| if (library === "iconify") { | |
| return ( | |
| <Icon | |
| icon={name} | |
| width={currentSize} | |
| height={currentSize} | |
| color={currentColor} | |
| style={{ | |
| transform: ` | |
| rotate(${currentRotation}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| }} | |
| /> | |
| ); | |
| } | |
| // ----------------------------------- | |
| // 🔥 FONT AWESOME v5 + v6 (AUTO DETECT) | |
| // ----------------------------------- | |
| // ----------------------------------- | |
| // MATERIAL DESIGN ICONS | |
| if (library === "mdi") { | |
| return ( | |
| <Icon | |
| icon={`mdi:${name.toLowerCase()}`} | |
| width={currentSize} | |
| height={currentSize} | |
| color={currentColor} | |
| style={{ | |
| transform: ` | |
| rotate(${currentRotation}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| }} | |
| /> | |
| ); | |
| } | |
| // ----------------------------------- | |
| // 🔥 PHOSPHOR / LUCIDE / TABLER | |
| // ----------------------------------- | |
| // Convert "arrow-right" → "ArrowRight" | |
| const pascalName = name | |
| .split("-") | |
| .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) | |
| .join(""); | |
| let IconComponent = null; | |
| if (library === "ph") { | |
| IconComponent = PhosphorIcons[pascalName]; | |
| } | |
| // ----------------------------------- | |
| // Fallback to Iconify IF Component missing | |
| // ----------------------------------- | |
| if (!IconComponent) { | |
| return ( | |
| <Icon | |
| icon={`${library}:${name}`} | |
| width={currentSize} | |
| height={currentSize} | |
| color={currentColor} | |
| style={{ | |
| transform: ` | |
| rotate(${currentRotation}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| }} | |
| /> | |
| ); | |
| } | |
| return ( | |
| <IconComponent | |
| size={currentSize} | |
| color={currentColor} | |
| style={{ | |
| transform: ` | |
| rotate(${currentRotation}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| }} | |
| /> | |
| ); | |
| })()} | |
| </div> | |
| <div className={styles["icon-name"]}>{previewDisplayName}</div> | |
| <div className={styles["icon-library"]}>{previewLibraryName}</div> | |
| </div> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <button | |
| className={styles["property-button-primary"]} | |
| onClick={() => setActiveSection("icon-picker")} | |
| > | |
| <ArrowsClockwiseIcon | |
| className={`${styles.ph} ${styles["ph-arrows-clockwise"]}`} | |
| /> | |
| Replace Icon | |
| </button> | |
| </div> | |
| </div> | |
| {/* ===== SIZE ===== */} | |
| <div className={styles["property-group"]}> | |
| <div className={styles["property-group-title"]}> | |
| <ResizeIcon className={`${styles.ph} ${styles["ph-resize"]}`} /> | |
| <span>Size</span> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Icon Size</label> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <span>Size</span> | |
| <span className={styles["slider-value"]}>{currentSize}</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="12" | |
| max="50" | |
| value={currentSize} | |
| onChange={(e) => { | |
| const newSize = Number(e.target.value); | |
| setCurrentSize(newSize); | |
| if (!selectedElement?.dataProp) return; | |
| const iframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!iframe?.contentWindow) return; | |
| // ALWAYS USE FRESH VALUES | |
| const updatedStyle = { | |
| color: currentColor, | |
| fontSize: `${newSize}px`, | |
| transform: ` | |
| rotate(${currentRotation || 0}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| }; | |
| iframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_ICON_STYLE", | |
| payload: { | |
| dataProp: selectedElement.dataProp, | |
| style: updatedStyle, | |
| icon: { | |
| library: selectedIcon?.library, | |
| name: selectedIcon?.name, | |
| classList: selectedIcon?.classList | |
| } | |
| } | |
| }, | |
| "*" | |
| ); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* ===== COLOR ===== */} | |
| <div className={styles["property-group"]}> | |
| <div className={styles["property-group-title"]}> | |
| <PaletteIcon className={`${styles.ph} ${styles["ph-palette"]}`} /> | |
| <span>Color</span> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Icon Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| {/* === COLOR PICKER INPUT === */} | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| value={currentColor} | |
| onChange={(e) => { | |
| const newColor = e.target.value; | |
| setCurrentColor(newColor); | |
| if (!selectedElement?.dataProp) return; | |
| const iframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!iframe?.contentWindow) return; | |
| const updatedStyle = { | |
| color: newColor, | |
| fontSize: `${currentSize}px`, | |
| transform: ` | |
| rotate(${currentRotation}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| }; | |
| iframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_ICON_STYLE", | |
| payload: { | |
| dataProp: selectedElement.dataProp, | |
| style: updatedStyle, | |
| icon: selectedIcon, | |
| }, | |
| }, | |
| "*" | |
| ); | |
| }} | |
| /> | |
| {/* === HEX VALUE INPUT === */} | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={currentColor} | |
| onChange={(e) => { | |
| const newColor = e.target.value; | |
| setCurrentColor(newColor); | |
| if (!/^#([0-9A-Fa-f]{3}){1,2}$/.test(newColor)) return; // allow only valid hex | |
| if (!selectedElement?.dataProp) return; | |
| const iframe = pageIframeRefs.current?.[selectedElement.instanceId]; | |
| if (!iframe?.contentWindow) return; | |
| const updatedStyle = { | |
| color: newColor, | |
| fontSize: `${currentSize}px`, | |
| transform: ` | |
| rotate(${currentRotation}deg) | |
| scaleX(${isFlippedHorizontal ? -1 : 1}) | |
| scaleY(${isFlippedVertical ? -1 : 1}) | |
| `, | |
| fontFamily: "inherit", | |
| }; | |
| iframe.contentWindow.postMessage( | |
| { | |
| type: "UPDATE_ICON_STYLE", | |
| payload: { | |
| dataProp: selectedElement.dataProp, | |
| style: updatedStyle, | |
| icon: selectedIcon, | |
| }, | |
| }, | |
| "*" | |
| ); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {ActiveSection === 'form' && ( | |
| <div> | |
| {/* Accordion Section 1: Form Info */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${activeAccordion === "form-info" ? styles["active"] : ""}`} | |
| onClick={() => toggleAccordion("form-info")} | |
| data-accordion="form-info" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <InfoIcon className={`${styles.ph} ${styles["ph-info"]}`} /> | |
| <span>Form Info</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${activeAccordion === "form-info" ? styles["rotated"] : ""}`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${activeAccordion === "form-info" ? styles["active"] : ""}`} | |
| id="form-info" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* Form ID */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Form ID</label> | |
| <div className={styles["property-display-box"]}> | |
| {formId || "No form selected"} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Accordion Section 2: Layout & Spacing */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "form-layout" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("form-layout")} | |
| data-accordion="form-layout" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <LayoutIcon className={`${styles.ph} ${styles["ph-layout"]}`} /> | |
| <span>Layout & Spacing</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "form-layout" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "form-layout" ? styles["active"] : "" | |
| }`} | |
| id="form-layout" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* Max Width */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Max Width (px)</label> | |
| <div className={styles["number-input-group"]}> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => { | |
| setFormMaxWidth(prev => { | |
| const v = Math.max(200, Number(prev) - 50); | |
| updateFormStyles({ maxWidth: v }); | |
| return v; | |
| }); | |
| }} | |
| > | |
| <MinusIcon className={styles.ph} /> | |
| </button> | |
| <input | |
| type="number" | |
| className={styles["property-input"]} | |
| value={formMaxWidth} | |
| onChange={(e) => { | |
| const v = Number(e.target.value); | |
| setFormMaxWidth(v); | |
| updateFormStyles({ maxWidth: v }); | |
| }} | |
| /> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => { | |
| setFormMaxWidth((prev) => { | |
| const v = Number(prev || 0) + 50; // ← NO LIMIT | |
| console.log('New Max Width:', v); | |
| updateFormStyles({ maxWidth: v }); | |
| return v; | |
| }); | |
| }} | |
| > | |
| <PlusIcon className={styles.ph} /> | |
| </button> | |
| <span className={styles["input-unit"]}>px</span> | |
| </div> | |
| </div> | |
| {/* Padding */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Padding (px)</label> | |
| <div className={styles["number-input-group"]}> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => { | |
| setFormPadding((prev) => { | |
| const v = Math.max(0, Number(prev) - 4); | |
| updateFormStyles({ padding: v }); // FIX | |
| return v; | |
| }); | |
| }} | |
| > | |
| <MinusIcon className={styles.ph} /> | |
| </button> | |
| <input | |
| type="number" | |
| className={styles["property-input"]} | |
| value={formPadding} | |
| onChange={(e) => { | |
| const v = Number(e.target.value); | |
| setFormPadding(v); | |
| updateFormStyles({ padding: v }); | |
| }} | |
| /> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => { | |
| setFormPadding((prev) => { | |
| const v = Math.min(100, Number(prev) + 4); | |
| updateFormStyles({ padding: v }); | |
| return v; | |
| }); | |
| }} | |
| > | |
| <PlusIcon className={styles.ph} /> | |
| </button> | |
| <span className={styles["input-unit"]}>px</span> | |
| </div> | |
| </div> | |
| {/* Border Radius */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Border Radius (px)</label> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <span className={styles["slider-value"]}>{formBorderRadius}px</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0" | |
| max="40" | |
| step="2" | |
| value={formBorderRadius} | |
| onChange={(e) => { | |
| const v = Number(e.target.value); | |
| setFormBorderRadius(v); | |
| updateFormStyles({ borderRadius: v }); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Alignment */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Alignment</label> | |
| <div className={styles["button-group"]}> | |
| {["left", "center", "right"].map((align) => ( | |
| <button | |
| key={align} | |
| className={`${styles["format-button"]} ${ | |
| formAlignment === align ? styles["active"] : "" | |
| }`} | |
| data-align={align} | |
| title={align.charAt(0).toUpperCase() + align.slice(1)} | |
| onClick={() => { | |
| setFormAlignment(align); | |
| updateFormStyles({ alignment: align }); | |
| }} | |
| > | |
| {align === "left" && <AlignLeftIcon className={styles.ph} />} | |
| {align === "center" && ( | |
| <AlignCenterVerticalIcon className={styles.ph} /> | |
| )} | |
| {align === "right" && <AlignRightIcon className={styles.ph} />} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Accordion Section 3: Background */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${activeAccordion === "form-background" ? styles["active"] : ""}`} | |
| onClick={() => toggleAccordion("form-background")} | |
| data-accordion="form-background" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <PaletteIcon className={`${styles.ph} ${styles["ph-palette"]}`} /> | |
| <span>Background</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${activeAccordion === "form-background" ? styles["rotated"] : ""}`} | |
| /> | |
| </button> | |
| <div className={`${styles["accordion-content"]} ${activeAccordion === "form-background" ? styles["active"] : ""}`} id="form-background"> | |
| <div className={styles["accordion-body"]}> | |
| {/* Background Color */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Background Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| value={formBackgroundColor} | |
| onChange={(e) => { | |
| const v = e.target.value; | |
| setFormBackgroundColor(v); | |
| updateFormStyles({ bgColor: v }); | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={formBackgroundColor} | |
| onChange={(e) => { | |
| const v = e.target.value; | |
| setFormBackgroundColor(v); | |
| updateFormStyles({ bgColor: v }); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Popular Presets */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Popular Presets</label> | |
| <div className={styles["form-color-presets"]}> | |
| {[ | |
| "#ffffff", | |
| "#F9FAFB", | |
| "linear-gradient(135deg, #F0FDF4 0%, #DCFCE7 100%)", | |
| "linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%)", | |
| "linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%)", | |
| "linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%)" | |
| ].map((color) => ( | |
| <div | |
| key={color} | |
| className={styles["form-color-preset"]} | |
| style={{ background: color }} | |
| onClick={() => { | |
| if (!color.includes("gradient")) { | |
| setFormBackgroundColor(color); // only hex allowed | |
| } | |
| updateFormStyles({ bgColor: color }); // gradient + hex allowed | |
| }} | |
| title={color} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Accordion Section 4: Border & Shadow */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${activeAccordion === "form-border" ? styles["active"] : ""}`} | |
| onClick={() => toggleAccordion("form-border")} | |
| data-accordion="form-border" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <FrameCornersIcon className={`${styles.ph} ${styles["ph-frame-corners"]}`} /> | |
| <span>Border & Shadow</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${activeAccordion === "form-border" ? styles["rotated"] : ""}`} | |
| /> | |
| </button> | |
| <div className={`${styles["accordion-content"]} ${activeAccordion === "form-border" ? styles["active"] : ""}`} id="form-border"> | |
| <div className={styles["accordion-body"]}> | |
| {/* Border Width */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Border Width (px)</label> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <span className={styles["slider-value"]}>{formBorderWidth}px</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0" | |
| max="10" | |
| step="1" | |
| value={formBorderWidth} | |
| onChange={(e) => { | |
| const v = Number(e.target.value); | |
| setFormBorderWidth(v); | |
| updateFormStyles({ borderWidth: v }); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Border Color */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Border Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| value={formBorderColor} | |
| onChange={(e) => { | |
| const v = e.target.value; | |
| setFormBorderColor(v); | |
| updateFormStyles({ borderColor: v }); | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={formBorderColor} | |
| onChange={(e) => { | |
| const v = e.target.value; | |
| setFormBorderColor(v); | |
| updateFormStyles({ borderColor: v }); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Shadow Intensity */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Shadow Intensity</label> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <span className={styles["slider-value"]}>{["None","Small","Medium","Large"][formShadow]}</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0" | |
| max="3" | |
| step="1" | |
| value={formShadow} | |
| onChange={(e) => { | |
| const v = Number(e.target.value); | |
| setFormShadow(v); | |
| updateFormStyles({ shadow: v }); | |
| }} | |
| /> | |
| </div> | |
| <div className={styles["text-muted"]} style={{ marginTop: "8px", fontSize: "11px" }}> | |
| 0 = None, 1 = Small, 2 = Medium, 3 = Large | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className={styles.accordion}> | |
| {/* Formatting */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "formatting" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("formatting")} | |
| data-accordion="formatting" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextAaIcon className={`${styles.ph} ${styles["ph-text-aa"]}`} /> | |
| <span>Formatting</span> | |
| </div> | |
| {/* Rotating caret */} | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "formatting" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "formatting" ? styles["active"] : "" | |
| }`} | |
| id="formatting" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["button-group"]}> | |
| {[ | |
| { key: "bold", icon: <TextBIcon />, title: "Bold (Ctrl+B)" }, | |
| { key: "italic", icon: <TextItalicIcon />, title: "Italic (Ctrl+I)" }, | |
| { key: "underline", icon: <TextUnderlineIcon />, title: "Underline (Ctrl+U)" }, | |
| { key: "strike", icon: <TextStrikethroughIcon />, title: "Strikethrough" }, | |
| { key: "superscript", icon: <TextSuperscriptIcon />, title: "Superscript" }, | |
| { key: "subscript", icon: <TextSubscriptIcon />, title: "Subscript" }, | |
| ].map(({ key, icon, title }) => ( | |
| <button | |
| key={key} | |
| className={`${styles["format-button"]} ${ | |
| activeFormats[key] ? styles["active"] : "" | |
| }`} | |
| data-command={key} | |
| title={title} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| applyFormattingText(key, true); | |
| }} | |
| > | |
| {icon} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Font & Size */} | |
| <div className={styles["accordion-section"]}> | |
| {/* Accordion Header */} | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "font" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("font")} | |
| data-accordion="font" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextTIcon className={`${styles.ph} ${styles["ph-text-t"]}`} /> | |
| <span>Font & Size</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "font" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| {/* Accordion Content */} | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "font" ? styles["active"] : "" | |
| }`} | |
| id="font" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* Font Family */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Font Family</label> | |
| <select | |
| className={styles["property-select"]} | |
| value={propertyValues.fontFamily || "inherit"} // fallback safe | |
| onChange={(e) => { | |
| const newVal = e.target.value; | |
| setPropertyValues((prev) => ({ ...prev, fontFamily: newVal })); | |
| applyFormatting("fontFamily", newVal); | |
| }} | |
| > | |
| <option value="inherit">Default (Inter)</option> | |
| <optgroup label="⭐ Web Safe Fonts"> | |
| <option value="Arial">Arial</option> | |
| <option value="Helvetica">Helvetica</option> | |
| <option value="Georgia">Georgia</option> | |
| <option value="Times New Roman">Times New Roman</option> | |
| <option value="Courier New">Courier New</option> | |
| </optgroup> | |
| </select> | |
| </div> | |
| {/* Font Size */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Font Size (px)</label> | |
| <div className={styles["number-input-group"]}> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => handleFontSizeChange(fontSize - 1)} | |
| > | |
| <MinusIcon className={`${styles.ph} ${styles["ph-minus"]}`} /> | |
| </button> | |
| <input | |
| type="number" | |
| className={styles["property-input"]} | |
| value={fontSize} | |
| min="8" | |
| max="120" | |
| onChange={(e) => | |
| handleFontSizeChange(parseInt(e.target.value, 10)) | |
| } | |
| /> | |
| <button | |
| className={styles["number-button"]} | |
| onClick={() => handleFontSizeChange(fontSize + 1)} | |
| > | |
| <PlusIcon className={`${styles.ph} ${styles["ph-plus"]}`} /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Colors */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "colors" ? styles["active"] : "" | |
| }`} | |
| onClick={() => { | |
| toggleAccordion("colors"); | |
| console.log("colors accordion clicked", activeAccordion); | |
| }} | |
| data-accordion="colors" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <PaletteIcon className={`${styles.ph} ${styles["ph-palette"]}`} /> | |
| <span>Colors</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "colors" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "colors" ? styles["active"] : "" | |
| }`} | |
| id="colors" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| {/* 🎨 Text Color */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Text Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| onChange={(e) => { | |
| const color = e.target.value.trim(); | |
| applyFormatting("color", color); | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={propertyValues.color} | |
| onChange={(e) => { | |
| const color = e.target.value.trim(); | |
| setPropertyValues((prev) => ({ ...prev, color })); | |
| }} | |
| onBlur={(e) => { | |
| let color = e.target.value.trim(); | |
| // 🎨 Convert rgb(255, 255, 255) → #ffffff | |
| if (color.startsWith("rgb")) { | |
| const match = color.match(/\d+/g); | |
| if (match) { | |
| const [r, g, b] = match.map(Number); | |
| color = `#${((1 << 24) + (r << 16) + (g << 8) + b) | |
| .toString(16) | |
| .slice(1) | |
| .toUpperCase()}`; | |
| } | |
| } | |
| if (/^#[0-9A-Fa-f]{6}$/.test(color)) { | |
| applyFormatting("color", color); | |
| } else { | |
| console.warn("⚠️ Invalid color format:", color); | |
| } | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| {/* 🎨 Background Color */} | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Background Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| onChange={(e) => { | |
| const bgColor = e.target.value.trim(); | |
| applyFormatting("backgroundColor", bgColor); | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| value={propertyValues.backgroundColor} | |
| onChange={(e) => { | |
| const bgColor = e.target.value.trim(); | |
| setPropertyValues((prev) => ({ ...prev, backgroundColor: bgColor })); | |
| }} | |
| onBlur={(e) => { | |
| let bgColor = e.target.value.trim(); | |
| // 🎨 Convert rgb(255, 255, 255) → #FFFFFF | |
| if (bgColor.startsWith("rgb")) { | |
| const match = bgColor.match(/\d+/g); | |
| if (match) { | |
| const [r, g, b] = match.map(Number); | |
| bgColor = `#${((1 << 24) + (r << 16) + (g << 8) + b) | |
| .toString(16) | |
| .slice(1) | |
| .toUpperCase()}`; | |
| } | |
| } | |
| // ✅ Validate and apply | |
| if (/^#[0-9A-Fa-f]{6}$/.test(bgColor)) { | |
| applyFormatting("backgroundColor", bgColor); | |
| } else { | |
| console.warn("⚠️ Invalid background color format:", bgColor); | |
| } | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Spacing */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "spacing" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("spacing")} | |
| data-accordion="spacing" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <TextAlignJustifyIcon className={` ${styles.ph} ${styles['ph-text-align-justify']}`}/> | |
| <span>Spacing</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "spacing" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "spacing" ? styles["active"] : "" | |
| }`} | |
| id="spacing" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Line Height</label> | |
| <span className={styles["slider-value"]}>{lineHeightValue}</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="0.5" | |
| max="3" | |
| step="0.1" | |
| value={lineHeightValue} | |
| onChange={(e) => { | |
| setLineHeightValue(e.target.value); | |
| applyFormatting("lineHeight", e.target.value); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <div className={styles["slider-group"]}> | |
| <div className={styles["slider-label-row"]}> | |
| <label className={styles["property-label"]}>Letter Spacing (px)</label> | |
| <span className={styles["slider-value"]}>{letterSpacingValue}</span> | |
| </div> | |
| <input | |
| type="range" | |
| className={styles["property-slider"]} | |
| min="-5" | |
| max="20" | |
| step="0.5" | |
| value={letterSpacingValue} | |
| onChange={(e) => { | |
| setLetterSpacingValue(e.target.value); | |
| applyFormatting("letterSpacing", e.target.value + "px"); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Link */} | |
| <div className={styles["accordion-section"]}> | |
| <button | |
| className={`${styles["accordion-header"]} ${ | |
| activeAccordion === "link" ? styles["active"] : "" | |
| }`} | |
| onClick={() => toggleAccordion("link")} | |
| data-accordion="link" | |
| > | |
| <div className={styles["accordion-title"]}> | |
| <LinkIcon className={`${styles.ph} ${styles['ph-link']}`}/> | |
| <span>Link</span> | |
| </div> | |
| <CaretDownIcon | |
| className={`${styles.ph} ${styles["ph-caret-down"]} ${ | |
| activeAccordion === "link" ? styles["rotated"] : "" | |
| }`} | |
| /> | |
| </button> | |
| <div | |
| className={`${styles["accordion-content"]} ${ | |
| activeAccordion === "link" ? styles["active"] : "" | |
| }`} | |
| id="link" | |
| > | |
| <div className={styles["accordion-body"]}> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Link URL</label> | |
| <input | |
| type="url" | |
| className={styles["property-input"]} | |
| placeholder="https://example.com" | |
| /> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label | |
| className={styles["property-label"]} | |
| style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" }} | |
| > | |
| <input type="checkbox" style={{ width: "auto" }} defaultChecked /> | |
| Open in new tab | |
| </label> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Link Color</label> | |
| <div className={styles["color-picker-group"]}> | |
| <input | |
| type="color" | |
| className={styles["color-picker"]} | |
| defaultValue="#1F3A65" | |
| /> | |
| <input | |
| type="text" | |
| className={styles["property-input"]} | |
| defaultValue="#1F3A65" | |
| /> | |
| </div> | |
| </div> | |
| <div className={styles["property-field"]}> | |
| <label className={styles["property-label"]}>Text Decoration</label> | |
| <select className={styles["property-select"]} defaultValue="underline"> | |
| <option value="underline">Underline</option> | |
| <option value="none">None</option> | |
| <option value="overline">Overline</option> | |
| <option value="line-through">Line Through</option> | |
| </select> | |
| </div> | |
| <div | |
| className={styles["button-group"]} | |
| style={{ marginTop: "16px", display: "flex", gap: "8px" }} | |
| > | |
| <button | |
| className={styles["property-button-primary"]} | |
| style={{ flex: 1 }} | |
| onClick={() => { | |
| const url = document.querySelector(`#link input[type="url"]`)?.value.trim() || "#"; | |
| const openInNewTab = document.querySelector(`#link input[type="checkbox"]`)?.checked; | |
| const color = document.querySelector(`#link input[type="color"]`)?.value; | |
| const textDecoration = document.querySelector(`#link select`)?.value; | |
| const target = openInNewTab ? "_blank" : "_self"; | |
| const linkData = { url, target, color, textDecoration }; | |
| applyFormatting("link", linkData); | |
| }} | |
| > | |
| <CheckIcon className={` ${styles.ph} ${styles['ph-check']}`}/> | |
| Apply Link | |
| </button> | |
| <button | |
| className={styles["property-button"]} | |
| style={{ flex: 1 }} | |
| onClick={() => { | |
| applyFormatting("removeLink", true); | |
| }} | |
| > | |
| <LinkBreakIcon className={` ${styles.ph} ${styles['ph-link-break']}`}/> | |
| Remove | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {ActiveSection === "icon-picker" && ( | |
| <div className={styles["icon-picker-modal"]}> | |
| <div className={styles["icon-picker-content"]}> | |
| {/* Modal Header */} | |
| <div className={styles["modal-header"]}> | |
| <h2 className={styles["modal-title"]}> | |
| <i className="ph ph-magnifying-glass"></i> | |
| Choose Icon | |
| </h2> | |
| <button | |
| className={styles["modal-close-btn"]} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| setActiveSection("icon") | |
| }} | |
| > | |
| <XIcon className={` ${styles.ph} ${styles['ph-x']}`}/> | |
| </button> | |
| </div> | |
| {/* Modal Body */} | |
| <div className={styles["modal-body"]}> | |
| {/* Search Bar */} | |
| <div className={styles["search-container"]}> | |
| <MagnifyingGlassIcon className={` ${styles.ph} ${styles['ph-magnifying-glass']} ${styles['search-icon']}`}/> | |
| <input | |
| type="text" | |
| className={styles["search-input"]} | |
| placeholder="Search 24,000+ icons..." | |
| value={search} | |
| onChange={(e) => setSearch(e.target.value)} | |
| /> | |
| <span className={styles["search-count"]}> | |
| Showing {filterIcons.length} icons | |
| </span> | |
| </div> | |
| {/* Library Tabs */} | |
| <div className={styles["library-tabs"]}> | |
| {[ | |
| { id: "popular", label: "Most Used" }, | |
| { id: "all", label: "All Libraries" }, | |
| ...Object.keys(ICON_LIBRARIES).map((lib) => ({ | |
| id: lib, | |
| label: ICON_LIBRARIES[lib].name, | |
| })), | |
| ].map((tab) => ( | |
| <button | |
| key={tab.id} | |
| className={`${styles["library-tab"]} ${ | |
| selectedLibrary === tab.id ? styles.active : "" | |
| }`} | |
| onClick={() => { | |
| setSelectedLibrary(tab.id); | |
| if (tab.id === "popular") { | |
| setIcons(POPULAR_ICONS); | |
| } else if (tab.id === "all") { | |
| loadAllLibraries(); | |
| } else { | |
| loadLibrary(ICON_LIBRARIES[tab.id].dataFile,tab.id); | |
| } | |
| }} | |
| > | |
| {tab.label} | |
| {ICON_LIBRARIES[tab.id]?.count && ( | |
| <span className={styles["library-count"]}> | |
| {ICON_LIBRARIES[tab.id].count / 1000}K | |
| </span> | |
| )} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Category Filter */} | |
| <div className={styles["category-filter"]}> | |
| {Object.keys(ICON_CATEGORIES).map((cat) => ( | |
| <button | |
| key={cat} | |
| className={`${styles["category-btn"]} ${ | |
| selectedCategory === cat ? styles.active : "" | |
| }`} | |
| onClick={() => setSelectedCategory(cat)} | |
| > | |
| {ICON_CATEGORIES[cat].name} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Icon Grid */} | |
| <div className={styles["icon-grid-container"]}> | |
| <div className={styles["icon-grid"]}> | |
| {filterIcons.length === 0 ? ( | |
| <div className={styles["loading-spinner"]}> | |
| <CircleNotchIcon className={` ${styles.ph} ${styles['ph-circle-notch']} ${styles.spin}`}/> | |
| <p>Loading icons...</p> | |
| </div> | |
| ) : ( | |
| filterIcons.map((icon) => ( | |
| <div | |
| key={`${icon.library}-${icon.name}`} | |
| className={`${styles["icon-grid-item"]} ${ | |
| selectedIcon?.name === icon.name && | |
| selectedIcon?.library === icon.library | |
| ? styles.selected | |
| : "" | |
| }`} | |
| onClick={(e) => handleSelect(icon, e)} | |
| > | |
| {/* ✅ Actual SVG Icon */} | |
| <Icon | |
| icon={ | |
| icon.library === "fa" | |
| ? icon.tags?.includes("brands") || ["facebook","twitter","instagram","linkedin","youtube","github"].includes(icon.name) | |
| ? "fa-brands:" + icon.name | |
| : "fa-solid:" + icon.name | |
| : `${ICON_LIBRARIES[icon.library]?.prefix || "ph"}:${icon.name}` | |
| } | |
| width={48} | |
| height={48} | |
| style={{ color: getLibraryColor(icon.library) }} | |
| className={styles["editable-icon"]} | |
| /> | |
| {/* Icon Label */} | |
| <span className={styles["icon-label"]}>{getIconDisplayName(icon.name)}</span> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| {/* Load More */} | |
| <div | |
| className={styles["load-more-container"]} | |
| style={{ display: "none" }} | |
| > | |
| <button className={styles["property-button"]} id="loadMoreBtn"> | |
| <ArrowDownIcon className={` ${styles.ph} ${styles['ph-arrow-down']}`}/> | |
| Load More Icons | |
| </button> | |
| </div> | |
| </div> | |
| {/* Modal Footer */} | |
| <div className={styles["modal-footer"]}> | |
| <div className={styles["selected-icon-preview"]}> | |
| <div className={styles["preview-icon"]}> | |
| <span | |
| className="iconify" | |
| id="previewIconElement" | |
| data-icon="ph:heart" | |
| style={{ fontSize: "48px" }} | |
| ></span> | |
| </div> | |
| <div className={styles["preview-info"]}> | |
| <span className={styles["preview-name"]} id="previewIconName"> | |
| Select an icon | |
| </span> | |
| <span | |
| className={styles["preview-library"]} | |
| id="previewIconLibrary" | |
| > | |
| Choose from grid above | |
| </span> | |
| </div> | |
| </div> | |
| <div className={styles["modal-actions"]}> | |
| <button | |
| className={styles["property-button"]} | |
| onClick={() => setActiveSection("icon")} | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| className={styles["property-button-primary"]} | |
| onClick={handleApply} | |
| disabled={!selectedIcon} | |
| > | |
| <ChecksIcon className={`${styles.ph} ${styles['ph-check']}`}/> | |
| Apply Icon | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {publishModal && ( | |
| <div className={styles["success-modal"]} id="successModal"> | |
| <div className={styles["success-modal-content"]}> | |
| <div className={styles["success-modal-header"]}> | |
| <CheckCircleIcon className={` ${styles.ph} ${styles['ph-check-circle']}`}/> | |
| <h2>Website Published Successfully</h2> | |
| </div> | |
| <p> | |
| Your website is now <strong>live</strong> and ready to welcome visitors from around the world. | |
| You can visit it at{" "} | |
| <strong> | |
| {`https://${subdomain}.${process.env.NEXT_PUBLIC_PUBLISH_URL}`} | |
| </strong> | |
| </p> | |
| <div className={styles["success-modal-footer"]}> | |
| <button className={styles["btn-close-success"]} onClick={()=>{ | |
| setPublishModal(false) | |
| }}>Continue Editing</button> | |
| <button className={styles["btn-visit-site"]} > | |
| <a | |
| href={`https://${subdomain}.${process.env.NEXT_PUBLIC_PUBLISH_URL}/${renderFileName}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className={styles['visit-link']} | |
| > | |
| Visit Site | |
| </a></button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {pageDeleteModal && ( | |
| <div className={` ${styles["delete-page-modal"]} ${styles.active}`} id="deletePageModal" style={{display: "flex"}}> | |
| <div className={styles["delete-page-modal-content"]}> | |
| <div className={styles["delete-page-modal-header"]}> | |
| <div className={styles["delete-page-modal-title-container"]}> | |
| <div className={styles["delete-page-modal-title"]}> | |
| <WarningIcon className={` ${styles.ph} ${styles['ph-warning']}`}/> | |
| Delete Page | |
| </div> | |
| <button className={styles["close-delete-page-modal"]} onClick={()=>setpageDeleteModal(false)}fdprocessedid="b3965"> | |
| <XIcon className={` ${styles.ph} ${styles['ph-x']}`}/> | |
| </button> | |
| </div> | |
| </div> | |
| <div className={styles["delete-page-modal-body"]}> | |
| <div className={styles["delete-warning-icon"]}> | |
| <TrashSimpleIcon className={` ${styles.ph} ${styles['ph-trash']}`}/> | |
| </div> | |
| <div className={styles["delete-page-message"]}> | |
| <h3>Are you sure you want to delete this page?</h3> | |
| <p>This action cannot be undone. The page and all its content will be permanently removed from your website.</p> | |
| </div> | |
| <div className={styles["page-to-delete"]} id="pageToDelete"> | |
| <div className={styles["page-to-delete-icon"]}> | |
| <HouseIcon className={` ${styles.ph} ${styles['ph-house']}`}/> | |
| </div> | |
| <div className={styles["page-to-delete-info"]}> | |
| <h4 id="deletePageName">{pageName}</h4> | |
| <span id="deletePageSlug">/{pageName}</span> | |
| </div> | |
| </div> | |
| <div className={styles["delete-consequences"]}> | |
| <div className={styles["delete-consequences-title"]}> | |
| <WarningCircleIcon className={` ${styles.ph} ${styles['ph-warning-circle']}`}/> | |
| What will happen: | |
| </div> | |
| <ul> | |
| <li> | |
| <XCircleIcon className={` ${styles.ph} ${styles['ph-x-circle']}`}/> | |
| The page will be removed from your website | |
| </li> | |
| <li> | |
| <XCircleIcon className={` ${styles.ph} ${styles['ph-x-circle']}`}/> | |
| All page content and components will be lost | |
| </li> | |
| <li> | |
| <XCircleIcon className={` ${styles.ph} ${styles['ph-x-circle']}`}/> | |
| Any links to this page will become broken | |
| </li> | |
| <li> | |
| <XCircleIcon className={` ${styles.ph} ${styles['ph-x-circle']}`}/> | |
| SEO settings and metadata will be deleted | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div className={styles["delete-page-modal-footer"]}> | |
| <button className={styles["btn-cancel-delete"]} onClick={()=>setpageDeleteModal(false)} fdprocessedid="c6ls4p"> | |
| <ArrowLeftIcon className={` ${styles.ph} ${styles['ph-arrow-left']}`}/> | |
| Keep Page | |
| </button> | |
| <button className={styles["btn-confirm-delete"]} id="confirmDeleteBtn" onClick={()=>handleDeletePage(pageId)} fdprocessedid="in1fzk"> | |
| <TrashIcon className={` ${styles.ph} ${styles['ph-trash']}`}/> | |
| Delete Forever | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {databaseModal && ( | |
| <div className={` ${styles["delete-page-modal"]} ${styles.active}`} id="deletePageModal" style={{display: "flex"}}> | |
| <div className={styles["delete-page-modal-content"]}> | |
| <div className={styles["delete-page-modal-header"]}> | |
| <div className={styles["delete-page-modal-title-container"]}> | |
| <div className={styles["delete-page-modal-title"]}> | |
| <WarningIcon className={` ${styles.ph} ${styles['ph-warning']}`}/> | |
| Delete Table | |
| </div> | |
| <button className={styles["close-delete-page-modal"]} onClick={()=>setDatabaseModal(false)}fdprocessedid="b3965"> | |
| <XIcon className={` ${styles.ph} ${styles['ph-x']}`}/> | |
| </button> | |
| </div> | |
| </div> | |
| <div className={styles["delete-page-modal-body"]}> | |
| <div className={styles["delete-warning-icon"]}> | |
| <div className={styles.ph}> | |
| <TrashIcon className={`${styles['ph-trash']}`}/> | |
| </div> | |
| </div> | |
| <div className={styles["delete-page-message"]}> | |
| <h3>Are you sure you want to delete this table?</h3> | |
| <p>This action cannot be undone. The page and all its content will be permanently removed from your website.</p> | |
| </div> | |
| <div className={styles["page-to-delete"]} id="pageToDelete"> | |
| <div className={styles["page-to-delete-icon"]}> | |
| <DatabaseIcon className={` ${styles.ph} ${styles['ph-house']}`}/> | |
| </div> | |
| <div className={styles["page-to-delete-info"]}> | |
| <h4 id="deletePageName">{tableName}</h4> | |
| <span id="deletePageSlug">/{tableName}</span> | |
| </div> | |
| </div> | |
| <div className={styles["delete-consequences"]}> | |
| <div className={styles["delete-consequences-title"]}> | |
| <WarningCircleIcon className={` ${styles.ph} ${styles['ph-warning-circle']}`}/> | |
| What will happen: | |
| </div> | |
| <ul> | |
| <li> | |
| <XCircleIcon className={` ${styles.ph} ${styles['ph-x-circle']}`}/> | |
| The table will be removed from your website | |
| </li> | |
| <li> | |
| <XCircleIcon className={` ${styles.ph} ${styles['ph-x-circle']}`}/> | |
| All page content and components will be lost | |
| </li> | |
| <li> | |
| <XCircleIcon className={` ${styles.ph} ${styles['ph-x-circle']}`}/> | |
| Any links to this page will become broken | |
| </li> | |
| <li> | |
| <XCircleIcon className={` ${styles.ph} ${styles['ph-x-circle']}`}/> | |
| SEO settings and metadata will be deleted | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div className={styles["delete-page-modal-footer"]}> | |
| <button className={styles["btn-cancel-delete"]} onClick={()=>setDatabaseModal(false)} fdprocessedid="c6ls4p"> | |
| <ArrowLeftIcon className={` ${styles.ph} ${styles['ph-arrow-left']}`}/> | |
| Keep Page | |
| </button> | |
| <button className={styles["btn-confirm-delete"]} id="confirmDeleteBtn" onClick={(e)=>deleteSchemaById(e,schemaId)} fdprocessedid="in1fzk"> | |
| <TrashIcon className={` ${styles.ph} ${styles['ph-trash']}`}/> | |
| Delete Forever | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {selectedComponentInstanceId && ( | |
| <button className={styles["properties-toggle"]} onClick={()=>{ | |
| setSliderToggle(true); | |
| setSliders('sliders'); | |
| }}> | |
| <SlidersIcon className={` ${styles.ph} ${styles['ph-sliders']}`}/> | |
| </button> | |
| )} | |
| {showModel && ( | |
| <div className={`${styles['component-search-modal']} ${styles.active}`} style={{ display: 'flex' }}> | |
| <div className={styles["modal-content"]}> | |
| <div className={styles["modal-header"]}> | |
| <div className={styles["modal-title"]}>Add Component</div> | |
| <button className={styles["close-modal-btn"]} onClick={() => { | |
| setShowModel(false); | |
| setDebouncedSearchTerm(''); | |
| setSearchTerm(''); | |
| setResetIcon(false) | |
| }}> | |
| <XIcon className={` ${styles.ph} ${styles['ph-x']}`} /> | |
| </button> | |
| </div> | |
| <div className={styles["modal-search"]}> | |
| <div className={styles["search-box"]}> | |
| <MagnifyingGlassIcon size={18} className={`${styles.ph} ${styles["ph-magnifying-glass"]}`} /> | |
| <input | |
| placeholder="Search components..." | |
| value={searchTerm} | |
| onChange={handleSearchChange} | |
| /> | |
| {resetIcon && ( | |
| <XIcon className={`${styles.ph} ${styles['ph-x']} ${styles['reset-icon']} ${styles.show}`} onClick={()=>{ | |
| setDebouncedSearchTerm(''); | |
| setSearchTerm(''); | |
| setResetIcon(false) | |
| }}/> | |
| )} | |
| </div> | |
| </div> | |
| <div className={styles["modal-components"]}> | |
| {loadingComponents ? ( | |
| <div className={styles["loader-container"]}> | |
| <div className={styles.spinner}></div> | |
| <p>Loading components, please wait...</p> | |
| </div> | |
| ) : Object.keys(componentBy).length > 0 ? ( | |
| Object.entries(componentBy).map(([type, components]) => | |
| components.length > 0 && ( | |
| <div className={styles["modal-component-category"]} key={type}> | |
| <div className={styles["modal-category-header"]}> | |
| {type === "SEARCH_RESULTS" ? "Search Results" : type} | |
| </div> | |
| <div className={styles["modal-component-grid"]}> | |
| {components.map((component) => ( | |
| <div | |
| className={styles["modal-component-item"]} | |
| key={component._id} | |
| onClick={() => { | |
| handleComponentClick(component); | |
| setDebouncedSearchTerm(""); | |
| setSearchTerm(""); | |
| setResetIcon(false); | |
| }} | |
| > | |
| <div className={styles["modal-component-icon"]}> | |
| <LayoutIcon /> | |
| </div> | |
| <div className={styles["modal-component-name"]}> | |
| {component.name} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| ) | |
| ) : ( | |
| <div className={styles["no-components-message"]}> | |
| No components found. | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| ) | |
| } | |
| export default WebsiteBuilder |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment