Skip to content

Instantly share code, notes, and snippets.

@bharatsewani1993
Created January 9, 2026 18:07
Show Gist options
  • Select an option

  • Save bharatsewani1993/040aaf2d03900bc1ed9f328298cfcfc7 to your computer and use it in GitHub Desktop.

Select an option

Save bharatsewani1993/040aaf2d03900bc1ed9f328298cfcfc7 to your computer and use it in GitHub Desktop.
This is code for NCV PAGE BUILDER SCREEN.
'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