UNPKG

seat-picker

Version:

> 📚 [Full Documentation & Live Demo](https://seat-picker-docs.vercel.app/)

1,382 lines (1,377 loc) • 138 kB
import React, { useRef, useState, useEffect, useCallback } from 'react'; import { fabric } from 'fabric'; import { create } from 'zustand'; import { v4 } from 'uuid'; import { LuFolderOpen, LuSave, LuDownload, LuMousePointer, LuGrid2X2, LuLayoutDashboard, LuPlus, LuUndo, LuRedo, LuScissors, LuCopy, LuClipboardCheck, LuTrash2, LuZoomOut, LuZoomIn, LuLock, LuCircleFadingPlus, LuCommand, LuX } from 'react-icons/lu'; import { RiText, RiShapeLine, RiApps2AddLine, RiLockUnlockLine } from 'react-icons/ri'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; // src/components/index.tsx var useEventGuiStore = create((set, get) => ({ // ::::::::::::::::::: Loading state loading: false, // ::::::::::::::::::: Canvas state canvas: null, setCanvas: (canvas) => set({ canvas }), // ::::::::::::::::::: Seat states seats: [], // ::::::::::::::::::: Add Seat addSeat: (seat) => set((state) => ({ seats: [...state.seats, seat] })), // ::::::::::::::::::: Update Seat updateSeat: (id, updates) => set((state) => ({ seats: state.seats.map( (seat) => seat.id === id ? { ...seat, ...updates } : seat ) })), // ::::::::::::::::::: Delete Seat deleteSeat: (id) => set((state) => ({ seats: state.seats.filter((seat) => seat.id !== id) })), // ::::::::::::::::::: Selected Seat ID selectedSeatIds: [], setSelectedSeatIds: (ids) => set({ selectedSeatIds: ids }), // ::::::::::::::::::: Multiple seat creation mode state isMultipleSeatMode: false, setIsMultipleSeatMode: (isCreating) => set({ isMultipleSeatMode: isCreating }), // ::::::::::::::::::: Zone states zones: [{ id: v4(), name: "Ground floor", isChecked: true }], // ::::::::::::::::::: Add zone addZone: (name) => set((state) => ({ zones: [...state.zones, { id: v4(), name, isChecked: true }] })), // ::::::::::::::::::: Update zone updateZone: (id, updates) => set((state) => ({ zones: state.zones.map( (zone) => zone.id === id ? { ...zone, ...updates } : zone ) })), // ::::::::::::::::::: Delete zone deleteZone: (id) => set((state) => ({ zones: state.zones.filter((zone) => zone.id !== id) })), // ::::::::::::::::::: Zoom level state zoomLevel: 100, setZoomLevel: (level) => set({ zoomLevel: level }), // ::::::::::::::::::: Mode state toolMode: "select", setToolMode: (mode) => set({ toolMode: mode }), // ::::::::::::::::::: Action state toolAction: null, setToolAction: (action) => set({ toolAction: action }), // ::::::::::::::::::: Clipboard state clipboard: null, setClipboard: (objects) => set({ clipboard: objects }), lastClickedPoint: null, setLastClickedPoint: (point) => set({ lastClickedPoint: point }), // ::::::::::::::::::::: Undo/redo functionality undoStack: [], redoStack: [], addToUndoStack: (state) => { const { loading, undoStack } = get(); const lastState = undoStack[undoStack.length - 1]; if (lastState !== state && !loading) { set((prevState) => ({ undoStack: [...prevState.undoStack, state], redoStack: [] })); } }, // ::::::::::::::: Function: UNDO undo: () => { const { canvas, undoStack, redoStack } = get(); if (undoStack.length > 1 && canvas) { set({ loading: true }); const currentState = JSON.stringify(canvas.toJSON()); const previousState = undoStack[undoStack.length - 2]; canvas.loadFromJSON(previousState, () => { canvas.getObjects().forEach((obj) => { if (obj instanceof fabric.Circle) { obj.setControlsVisibility({ mt: false, mb: false, ml: false, mr: false }); } }); canvas.renderAll(); set({ undoStack: undoStack.slice(0, -1), redoStack: [currentState, ...redoStack] }); }); set({ loading: false }); } }, // :::::::::::::: Function: REDO redo: () => { const { canvas, undoStack, redoStack } = get(); if (redoStack.length > 0 && canvas) { set({ loading: true }); const nextState = redoStack[0]; canvas.loadFromJSON(nextState, () => { canvas.getObjects().forEach((obj) => { if (obj instanceof fabric.Circle) { obj.setControlsVisibility({ mt: false, mb: false, ml: false, mr: false }); } }); canvas.renderAll(); const currentState = JSON.stringify( canvas.toJSON([ "id", "borderColor", "borderDashArray", "cornerColor", "cornerSize", "cornerStrokeColor", "transparentCorners", "rx", "ry" ]) ); set({ undoStack: [...undoStack, currentState], redoStack: redoStack.slice(1) }); }); set({ loading: false }); } }, snapEnabled: false, setSnapEnabled: (enabled) => set({ snapEnabled: enabled }) })); // src/components/createObject/applyCustomStyles.ts function applyCustomStyles(obj) { obj.set({ borderColor: "green", borderDashArray: [2, 4], padding: 2, cornerColor: "lightblue", cornerSize: 5, cornerStrokeColor: "blue", transparentCorners: false, strokeUniform: true }); obj.setControlsVisibility && obj.setControlsVisibility({ mt: false, mb: false, ml: false, mr: false }); } // src/hooks/useClipboardActions.ts var useClipboardActions = () => { const { canvas, clipboard, setClipboard, lastClickedPoint, setToolAction } = useEventGuiStore(); const copySelectedObjects = async () => { if (!canvas) return; const activeObjects = canvas.getActiveObjects(); if (activeObjects.length === 0) return; setToolAction("copy"); const clonedObjects = []; for (const obj of activeObjects) { await new Promise((resolve) => { obj.clone((cloned) => { if ("id" in cloned) cloned.id = v4(); clonedObjects.push(cloned); resolve(); }); }); } setClipboard(clonedObjects); }; const cutSelectedObjects = async () => { if (!canvas) return; const activeObjects = canvas.getActiveObjects(); if (activeObjects.length === 0) return; setToolAction("cut"); const clonedObjects = []; for (const obj of activeObjects) { await new Promise((resolve) => { obj.clone((cloned) => { if ("id" in cloned) cloned.id = v4(); clonedObjects.push(cloned); resolve(); }); }); } setClipboard(clonedObjects); canvas.remove(...activeObjects); canvas.discardActiveObject(); canvas.renderAll(); }; const pasteObjects = async () => { if (!canvas || !clipboard || !lastClickedPoint) return; setToolAction("paste"); const pastedObjects = []; for (const obj of clipboard) { await new Promise((resolve) => { obj.clone((cloned) => { if ("id" in cloned) cloned.id = v4(); applyCustomStyles(cloned); cloned.set({ left: (cloned.left || 0) + 20, top: (cloned.top || 0) + 20, evented: true }); canvas.add(cloned); pastedObjects.push(cloned); resolve(); }); }); } if (pastedObjects.length === 1) { canvas.setActiveObject(pastedObjects[0]); } else if (pastedObjects.length > 1) { const selection = new fabric.ActiveSelection(pastedObjects, { canvas }); canvas.setActiveObject(selection); } canvas.requestRenderAll(); }; return { copySelectedObjects, cutSelectedObjects, pasteObjects }; }; var useClipboardActions_default = useClipboardActions; var useUndoRedo = () => { const { canvas, addToUndoStack, undo, redo, undoStack } = useEventGuiStore(); const handleObjectModified = useCallback(() => { if (canvas) { const jsonState = JSON.stringify( canvas.toJSON([ "id", "borderColor", "borderDashArray", "cornerColor", "cornerSize", "cornerStrokeColor", "transparentCorners", "rx", "ry" ]) ); addToUndoStack(jsonState); } }, [canvas, addToUndoStack]); useEffect(() => { if (!canvas) return; const eventsToListen = [ "object:modified", "object:added", "object:removed" ]; eventsToListen.forEach((event) => { canvas.on(event, handleObjectModified); }); return () => { eventsToListen.forEach((event) => { canvas.off(event, handleObjectModified); }); }; }, [canvas, addToUndoStack]); return { undo, redo }; }; var useUndoRedo_default = useUndoRedo; function useLockSelection(canvas) { const isSelectionLocked = useCallback(() => { if (!canvas) return false; const selected = canvas.getActiveObjects(); return selected.length > 0 && selected.every((obj) => obj.lockMovementX && obj.lockMovementY); }, [canvas]); const toggleLockSelection = useCallback(() => { if (!canvas) return; const selected = canvas.getActiveObjects(); if (selected.length === 0) return; const shouldLock = !isSelectionLocked(); selected.forEach((obj) => { obj.set({ lockMovementX: shouldLock, lockMovementY: shouldLock }); }); canvas.requestRenderAll(); if (selected.length > 1) { canvas.discardActiveObject(); const group = new fabric.ActiveSelection(selected, { canvas }); canvas.setActiveObject(group); canvas.requestRenderAll(); } canvas.fire("lock:changed"); }, [canvas, isSelectionLocked]); const [selectionVersion, setSelectionVersion] = useState(0); useEffect(() => { if (!canvas) return; const update = () => setSelectionVersion((v) => v + 1); canvas.on("selection:created", update); canvas.on("selection:updated", update); canvas.on("lock:changed", update); return () => { canvas.off("selection:created", update); canvas.off("selection:updated", update); canvas.off("lock:changed", update); }; }, [canvas]); return { isSelectionLocked, toggleLockSelection, selectionVersion }; } var Modal = ({ open, onClose, title, children }) => { if (!open) return null; return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/30", children: /* @__PURE__ */ jsxs("div", { className: "min-w-[320px] max-w-[90vw] rounded-lg bg-white p-6 shadow-lg", children: [ title && /* @__PURE__ */ jsx("h2", { className: "mb-4 text-lg font-semibold", children: title }), children, /* @__PURE__ */ jsx( "button", { className: "mt-4 w-full rounded border border-solid border-gray-300 bg-gray-200 px-4 py-1 text-sm hover:bg-gray-300", onClick: onClose, children: "Cancel" } ) ] }) }); }; var DefaultSeatModal = ({ selectedSeat, setSelectedSeat, mergedLabels, handleSeatAction }) => { return /* @__PURE__ */ jsx( Modal, { open: !!selectedSeat, onClose: () => setSelectedSeat(null), title: "Seat Details", children: selectedSeat && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-gray-600", children: mergedLabels.seatNumber }), /* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: selectedSeat.number }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-gray-600", children: mergedLabels.category }), /* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: selectedSeat.category }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-gray-600", children: mergedLabels.price }), /* @__PURE__ */ jsxs("p", { className: "text-lg font-semibold", children: [ selectedSeat.currencySymbol, selectedSeat.price, " ", /* @__PURE__ */ jsxs("span", { className: "text-sm text-gray-500", children: [ "(", selectedSeat.currencyCode, ")" ] }) ] }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-gray-600", children: mergedLabels.status }), /* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: selectedSeat.status }) ] }) ] }), /* @__PURE__ */ jsxs("div", { className: "mt-6 flex gap-3", children: [ /* @__PURE__ */ jsx( "button", { onClick: () => handleSeatAction("buy"), className: "flex-1 rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-400", children: mergedLabels.buyButton } ), /* @__PURE__ */ jsx( "button", { onClick: () => setSelectedSeat(null), className: "flex-1 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400", children: mergedLabels.cancelButton } ) ] }) ] }) } ); }; var Modal_default = Modal; var ExportModal = ({ open, onClose, fileName, setFileName, onExport }) => /* @__PURE__ */ jsx(Modal_default, { open, onClose, title: "Export as JSON", children: /* @__PURE__ */ jsxs("form", { onSubmit: onExport, className: "flex flex-col gap-4", children: [ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-gray-700", children: "File Name" }), /* @__PURE__ */ jsx( "input", { type: "text", value: fileName, onChange: (e) => setFileName(e.target.value), className: "rounded-md border border-gray-300 border-solid bg-gray-100 px-3 py-2 text-sm text-gray-800 focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-300", placeholder: "seats.json", required: true } ), /* @__PURE__ */ jsx( "button", { type: "submit", className: "rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-400", children: "Export" } ) ] }) }); var OpenFileModal = ({ open, onClose, file, setFile, error, onFileChange, onDrop, onDragOver, onSubmit }) => /* @__PURE__ */ jsx( Modal_default, { open, onClose: () => { onClose(); setFile(null); }, title: "Open Seating Arrangement", children: /* @__PURE__ */ jsxs("form", { onSubmit, className: "flex flex-col gap-4", children: [ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-gray-700", children: "Select or drag a JSON file" }), /* @__PURE__ */ jsxs( "div", { className: `flex flex-col items-center justify-center rounded-lg border-2 border-dashed ${error ? "border-red-400" : "border-gray-300"} bg-gray-100 px-6 py-8 transition-colors hover:bg-gray-200`, onDrop, onDragOver, children: [ /* @__PURE__ */ jsx( "input", { type: "file", accept: "application/json,.json", onChange: onFileChange, className: "hidden", id: "open-file-input" } ), /* @__PURE__ */ jsx( "label", { htmlFor: "open-file-input", className: "cursor-pointer text-sm rounded-md bg-gray-700 px-4 py-2 text-white shadow-sm transition-colors hover:bg-gray-800", children: file ? "Change File" : "Choose File" } ), /* @__PURE__ */ jsx("span", { className: "mt-2 text-xs text-gray-500", children: "or drag and drop here" }), file && /* @__PURE__ */ jsx("span", { className: "mt-2 text-sm font-medium text-gray-700", children: file.name }), error && /* @__PURE__ */ jsx("span", { className: "mt-2 text-xs text-red-500", children: error }) ] } ), /* @__PURE__ */ jsx( "button", { type: "submit", className: "rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-gray-800 disabled:bg-gray-300", disabled: !file, children: "Open" } ) ] }) } ); var typeStyles = { success: "bg-green-500/50 border-green-600", error: "bg-red-600 text-white", info: "bg-blue-600 text-white" }; var Toast = ({ open, message, type = "info", onClose }) => { if (!open) return null; return /* @__PURE__ */ jsxs( "div", { className: `fixed bottom-6 left-1/2 z-[100] flex h-12 -translate-x-1/2 transform items-center justify-center rounded-md px-6 shadow-lg backdrop-blur-sm ${typeStyles[type]} border border-solid`, role: "alert", onClick: onClose, children: [ /* @__PURE__ */ jsx("span", { children: message }), /* @__PURE__ */ jsx( "button", { className: "ease-250 ml-2 -mr-2 rounded border border-solid border-black/20 p-1 text-black/40 hover:border-black/40 hover:text-black/80 active:scale-110", onClick: onClose, children: /* @__PURE__ */ jsx(LuX, {}) } ) ] } ); }; var Toast_default = Toast; var Toolbar = ({ onSave, onBgLayout }) => { const { toolMode, setToolMode, toolAction, setToolAction, canvas, snapEnabled, setSnapEnabled } = useEventGuiStore(); const [zoomLevel, setZoomLevel] = useState(100); const { copySelectedObjects, cutSelectedObjects, pasteObjects } = useClipboardActions_default(); const { undo, redo } = useUndoRedo_default(); const { isSelectionLocked, toggleLockSelection } = useLockSelection(canvas); const [showExportModal, setShowExportModal] = useState(false); const [exportFileName, setExportFileName] = useState("seats.json"); const [showOpenModal, setShowOpenModal] = useState(false); const [openFile, setOpenFile] = useState(null); const [openFileError, setOpenFileError] = useState(""); const [showToast, setShowToast] = useState(false); const [toastMsg, setToastMsg] = useState(""); const [toastType, setToastType] = useState( "info" ); useEffect(() => { if (!canvas) return; const zoom = zoomLevel / 100; canvas.setZoom(zoom); const viewportWidth = canvas.getWidth(); const viewportHeight = canvas.getHeight(); const contentWidth = canvas.width * zoom; const contentHeight = canvas.height * zoom; canvas.absolutePan({ x: (viewportWidth - contentWidth) / 2, y: (viewportHeight - contentHeight) / 2 }); canvas.renderAll(); }, [zoomLevel, canvas]); const handleExport = (e) => { e.preventDefault(); if (!canvas) return; const json = JSON.stringify(canvas.toJSON()); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = exportFileName.endsWith(".json") ? exportFileName : exportFileName + ".json"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setShowExportModal(false); }; const toggleMultipleSeatMode = () => { setToolMode(toolMode === "multiple-seat" ? "select" : "multiple-seat"); }; const handleFileChange = (e) => { var _a; const file = (_a = e.target.files) == null ? void 0 : _a[0]; if (file) setOpenFile(file); }; const handleDrop = (e) => { e.preventDefault(); if (e.dataTransfer.files && e.dataTransfer.files[0]) { setOpenFile(e.dataTransfer.files[0]); } }; const handleDragOver = (e) => { e.preventDefault(); }; const handleOpenFile = async (e) => { e.preventDefault(); if (!canvas || !openFile) return; try { const text = await openFile.text(); const json = JSON.parse(text); canvas.loadFromJSON(json, () => { canvas.getObjects().forEach((obj) => { if (obj.type === "circle" || obj.type === "rect" || obj.type === "i-text") { applyCustomStyles(obj); } }); canvas.renderAll(); setShowOpenModal(false); setOpenFile(null); setToastMsg("Seating arrangement loaded!"); setToastType("success"); setShowToast(true); }); } catch (err) { setOpenFileError( "Invalid JSON file. Please select a valid seating arrangement file." ); setToastMsg("Invalid JSON file."); setToastType("error"); setShowToast(true); } }; const handleZoomIn = () => { setZoomLevel(Math.min(zoomLevel + 10, 120)); }; const handleZoomOut = () => { setZoomLevel(Math.max(zoomLevel - 10, 80)); }; const buttonGroups = [ [ { icon: LuFolderOpen, tooltip: "Open File", onClick: () => setShowOpenModal(true), state: false }, { icon: LuSave, tooltip: "Save", onClick: () => { if (canvas && onSave) { const json = { type: "canvas", ...canvas.toJSON(["customType", "seatData", "zoneData"]) }; onSave(json); } }, state: false }, { icon: LuDownload, tooltip: "Download", onClick: () => setShowExportModal(true), state: false } ], [ { icon: LuMousePointer, tooltip: "Select", onClick: () => setToolMode("select"), state: toolMode === "select" }, { icon: LuGrid2X2, tooltip: snapEnabled ? "Remove Grid" : "Smart Grid", onClick: () => setSnapEnabled(!snapEnabled), state: snapEnabled }, { icon: LuLayoutDashboard, tooltip: "Layout View", onClick: onBgLayout, state: false } ], [ { icon: RiText, tooltip: "Add Text", onClick: () => setToolMode("text"), state: toolMode === "text" }, { icon: RiShapeLine, tooltip: "Add Square", onClick: () => setToolMode("shape-square"), state: toolMode === "shape-square" }, { icon: LuPlus, tooltip: "Add Seat", onClick: () => setToolMode("one-seat"), state: toolMode === "one-seat" }, { icon: RiApps2AddLine, tooltip: "Add Rows", onClick: toggleMultipleSeatMode, state: toolMode === "multiple-seat" } ], [ { icon: LuUndo, tooltip: "Undo", onClick: undo, state: false }, { icon: LuRedo, tooltip: "Redo", onClick: redo, state: false } ], [ { icon: LuScissors, tooltip: "Cut", onClick: cutSelectedObjects, state: toolAction === "cut" }, { icon: LuCopy, tooltip: "Copy", onClick: copySelectedObjects, state: toolAction === "copy" }, { icon: LuClipboardCheck, tooltip: "Paste", onClick: pasteObjects, state: toolAction === "paste" }, { icon: LuTrash2, tooltip: "Delete", onClick: () => setToolAction("delete"), state: false } ] ]; return /* @__PURE__ */ jsxs("div", { className: "fixed left-0 top-0 z-[200] flex w-full items-center justify-center gap-1 bg-white px-[1rem] py-[0.5rem] shadow", children: [ /* @__PURE__ */ jsx("div", { className: "flex-1" }), buttonGroups.map((group, groupIdx) => /* @__PURE__ */ jsxs(React.Fragment, { children: [ groupIdx > 0 && /* @__PURE__ */ jsx(Separator, {}), group.map((item, idx) => /* @__PURE__ */ jsx( Button, { icon: /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4" }), tooltip: item.tooltip, onClick: item.onClick, state: item.state }, `button-${groupIdx}-${idx}` )) ] }, groupIdx)), /* @__PURE__ */ jsx(Separator, {}), /* @__PURE__ */ jsx( Button, { icon: /* @__PURE__ */ jsx(LuZoomOut, { className: "h-4 w-4" }), tooltip: "Zoom Out", onClick: handleZoomOut } ), /* @__PURE__ */ jsxs("div", { className: "flex h-8 w-12 items-center justify-center text-sm font-medium", children: [ zoomLevel, "%" ] }), /* @__PURE__ */ jsx( Button, { icon: /* @__PURE__ */ jsx(LuZoomIn, { className: "h-4 w-4" }), tooltip: "Zoom In", onClick: handleZoomIn } ), /* @__PURE__ */ jsx("div", { className: "flex-1" }), /* @__PURE__ */ jsx( Button, { icon: isSelectionLocked() ? /* @__PURE__ */ jsx(LuLock, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(RiLockUnlockLine, { className: "h-4 w-4" }), tooltip: isSelectionLocked() ? "Unlock Selection" : "Lock Selection", onClick: toggleLockSelection } ), /* @__PURE__ */ jsx( ExportModal, { open: showExportModal, onClose: () => setShowExportModal(false), fileName: exportFileName, setFileName: setExportFileName, onExport: handleExport } ), /* @__PURE__ */ jsx( OpenFileModal, { open: showOpenModal, onClose: () => { setShowOpenModal(false); setOpenFile(null); setOpenFileError(""); }, file: openFile, setFile: setOpenFile, error: openFileError, onFileChange: handleFileChange, onDrop: handleDrop, onDragOver: handleDragOver, onSubmit: handleOpenFile } ), /* @__PURE__ */ jsx( Toast_default, { open: showToast, message: toastMsg, type: toastType, onClose: () => setShowToast(false) } ) ] }); }; var toolbar_default = Toolbar; var Button = ({ icon, tooltip, state, ...props }) => { const [showTooltip, setShowTooltip] = useState(false); return /* @__PURE__ */ jsxs("div", { className: "relative", children: [ /* @__PURE__ */ jsx( "button", { className: `rounded-md p-2 hover:bg-gray-200/60 ${state ? "shadow-sm shadow-gray-400 ring-1 ring-gray-400" : ""} ease-250 active:bg-gray-200 `, onMouseEnter: () => setShowTooltip(true), onMouseLeave: () => setShowTooltip(false), ...props, children: icon } ), /* @__PURE__ */ jsx( "div", { className: `absolute left-1/2 -translate-x-1/2 transform ${showTooltip ? "top-[calc(100%+0.5rem)] opacity-100" : "top-[100%] opacity-0"} ease-250 whitespace-nowrap rounded bg-gray-200 px-2 py-1 text-[0.625rem] text-gray-900 shadow-md`, children: tooltip } ) ] }); }; var Separator = () => /* @__PURE__ */ jsx("div", { className: "mx-[1rem] h-6 w-px bg-gray-300 " }); var Select = ({ options, value, onChange, placeholder = "Select an option" }) => { const [isOpen, setIsOpen] = useState(false); const selectRef = useRef(null); const [dropdownPosition, setDropdownPosition] = useState( "bottom" ); const handleToggle = () => setIsOpen(!isOpen); const handleOptionClick = (optionValue) => { onChange(optionValue); setIsOpen(false); }; const handleClickOutside = (event) => { if (selectRef.current && !selectRef.current.contains(event.target)) { setIsOpen(false); } }; useEffect(() => { document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); const selectedOption = options.find((option) => option.value === value); const calculateDropdownPosition = () => { if (selectRef.current) { const { bottom } = selectRef.current.getBoundingClientRect(); const windowHeight = window.innerHeight; if (windowHeight - bottom < 120) { setDropdownPosition("top"); } else { setDropdownPosition("bottom"); } } }; useEffect(() => { if (isOpen) { calculateDropdownPosition(); } }, [isOpen]); return /* @__PURE__ */ jsxs("div", { ref: selectRef, className: "relative", children: [ /* @__PURE__ */ jsxs( "div", { className: "flex w-full cursor-pointer items-center justify-between rounded-md border border-solid border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-500", onClick: handleToggle, children: [ /* @__PURE__ */ jsx("span", { className: "block truncate", children: selectedOption ? selectedOption.label : placeholder }), /* @__PURE__ */ jsx("span", { className: "pointer-events-none", children: /* @__PURE__ */ jsx( "svg", { className: "h-5 w-5 text-gray-400", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: /* @__PURE__ */ jsx( "path", { d: "M7 7l3-3 3 3m0 6l-3 3-3-3", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" } ) } ) }) ] } ), /* @__PURE__ */ jsx( "div", { className: `absolute z-10 mt-1 w-full overflow-hidden rounded-[8px] border border-solid border-gray-300 bg-white shadow-lg ${dropdownPosition === "top" ? "bottom-full mb-1" : "top-full mt-1"} ${isOpen ? "user-select-auto visible opacity-100" : "user-select-none invisible select-none opacity-0"} ease-250`, children: /* @__PURE__ */ jsx("ul", { className: "max-h-60 overflow-auto text-base focus:outline-none sm:text-sm", children: options.map((option) => /* @__PURE__ */ jsxs( "li", { className: `relative cursor-pointer select-none py-2 pl-3 pr-9 ${value === option.value ? "bg-gray-100 text-gray-600" : "text-gray-900 hover:bg-gray-50"}`, onClick: () => handleOptionClick(option.value), children: [ /* @__PURE__ */ jsx("span", { className: "block truncate", children: option.label }), value === option.value && /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 right-0 flex items-center pr-4 text-gray-600", children: /* @__PURE__ */ jsx( "svg", { className: "h-5 w-5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsx( "path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" } ) } ) }) ] }, option.value )) }) } ) ] }); }; var select_default = Select; var useObjectProperties = (canvas, selectedObjects) => { const [properties, setProperties] = useState({ angle: 0, radius: 10, width: 100, height: 100, fill: "transparent", stroke: "#000000", text: "", fontSize: 20, fontWeight: "normal", fontFamily: "sans-serif", left: 0, top: 0 }); function getMergedValue(objs, key) { if (objs.length === 0) return ""; const first = objs[0][key]; return objs.every((obj) => obj[key] === first) ? first : "mixed"; } useEffect(() => { if (!selectedObjects || selectedObjects.length === 0) return; const objs = selectedObjects; setProperties({ angle: getMergedValue(objs, "angle"), radius: getMergedValue(objs, "radius"), width: getMergedValue(objs, "width") * (getMergedValue(objs, "scaleX") || 1), height: getMergedValue(objs, "height") * (getMergedValue(objs, "scaleY") || 1), fill: getMergedValue(objs, "fill"), stroke: getMergedValue(objs, "stroke"), text: getMergedValue(objs, "text"), fontSize: getMergedValue(objs, "fontSize"), fontWeight: getMergedValue(objs, "fontWeight"), fontFamily: getMergedValue(objs, "fontFamily"), left: getMergedValue(objs, "left"), top: getMergedValue(objs, "top"), rx: getMergedValue(objs, "rx"), ry: getMergedValue(objs, "ry"), seatNumber: getMergedValue(objs, "seatNumber"), category: getMergedValue(objs, "category"), price: getMergedValue(objs, "price"), status: getMergedValue(objs, "status"), currencySymbol: getMergedValue(objs, "currencySymbol"), currencyCode: getMergedValue(objs, "currencyCode"), currencyCountry: getMergedValue(objs, "currencyCountry") }); }, [selectedObjects]); return { properties, setProperties }; }; var useObjectUpdater = (canvas, setProperties, lockAspect = false) => { useEffect(() => { if (!canvas) return; const handleScaling = (e) => { const obj = e.target; if (!obj) return; if (obj.type === "circle") { const newRadius = (obj.radius || (obj.width ? obj.width / 2 : 0)) * (obj.scaleX || 1); obj.set({ radius: newRadius, scaleX: 1, scaleY: 1, width: newRadius * 2, height: newRadius * 2 }); obj.setCoords(); canvas.renderAll(); setProperties((prev) => ({ ...prev, radius: newRadius })); } else if (obj.type === "activeSelection" && "getObjects" in obj) { const selection = obj; const circles = selection.getObjects().filter( (o) => o.type === "circle" ); let radii = []; circles.forEach((circle) => { const newRadius = (circle.radius || (circle.width ? circle.width / 2 : 0)) * (circle.scaleX || 1); circle.set({ radius: newRadius, scaleX: 1, scaleY: 1, width: newRadius * 2, height: newRadius * 2 }); circle.setCoords(); radii.push(newRadius); }); canvas.renderAll(); const allSame = radii.every((r) => r === radii[0]); setProperties((prev) => ({ ...prev, radius: allSame ? radii[0] : "mixed" })); } }; canvas.on("object:scaling", handleScaling); return () => { canvas.off("object:scaling", handleScaling); }; }, [canvas, setProperties]); const updateObject = (updates) => { var _a, _b; if (!canvas) return; const activeObject = canvas.getActiveObject(); const activeObjects = canvas.getActiveObjects(); if (activeObjects.length === 0) return; if (activeObject && activeObject.type === "activeSelection") { const group = activeObject; if ("left" in updates || "top" in updates) { const deltaX = "left" in updates && typeof updates.left === "number" ? updates.left - ((_a = group.left) != null ? _a : 0) : 0; const deltaY = "top" in updates && typeof updates.top === "number" ? updates.top - ((_b = group.top) != null ? _b : 0) : 0; group.getObjects().forEach((obj) => { var _a2, _b2; obj.set({ left: ((_a2 = obj.left) != null ? _a2 : 0) + deltaX, top: ((_b2 = obj.top) != null ? _b2 : 0) + deltaY }); obj.setCoords(); }); group.setCoords(); canvas.renderAll(); setProperties((prev) => { var _a2, _b2; return { ...prev, left: (_a2 = updates.left) != null ? _a2 : prev.left, top: (_b2 = updates.top) != null ? _b2 : prev.top }; }); return; } if ("angle" in updates && typeof updates.angle === "number") { group.set("angle", updates.angle); group.setCoords(); canvas.renderAll(); setProperties((prev) => { var _a2; return { ...prev, angle: (_a2 = updates.angle) != null ? _a2 : prev.angle }; }); return; } } activeObjects.forEach((selectedObject) => { var _a2, _b2; const updatedProperties = {}; for (const [key, value] of Object.entries(updates)) { if (selectedObject[key] !== value) { updatedProperties[key] = value; } } if ("stroke" in updatedProperties && updatedProperties.stroke !== void 0) { updatedProperties.stroke = String(updatedProperties.stroke); } if (selectedObject.type === "circle") { if ("width" in updates || "height" in updates) { const currentRadius = selectedObject.radius || 0; const newRadius = updates.width ? updates.width / 2 : updates.height ? updates.height / 2 : currentRadius; selectedObject.set({ radius: newRadius, scaleX: 1, scaleY: 1, width: newRadius * 2, height: newRadius * 2 }); delete updatedProperties.width; delete updatedProperties.height; } } else { if ("width" in updates && updates.width !== void 0) { const renderedWidth = updates.width; const currentScaleX = selectedObject.scaleX || 1; selectedObject.set({ width: renderedWidth / currentScaleX, scaleX: 1, height: lockAspect ? renderedWidth / currentScaleX : selectedObject.height }); delete updatedProperties.width; } if ("height" in updates && updates.height !== void 0) { const renderedHeight = updates.height; const currentScaleY = selectedObject.scaleY || 1; selectedObject.set({ height: renderedHeight / currentScaleY, scaleY: 1, width: lockAspect ? renderedHeight / currentScaleY : selectedObject.width }); delete updatedProperties.height; } } selectedObject.set(updatedProperties); if (selectedObject.type === "i-text") { selectedObject.set({ scaleX: 1, scaleY: 1 }); } if (Object.prototype.hasOwnProperty.call(updates, "angle")) { selectedObject.setCoords(); const rect = selectedObject.getBoundingRect(); const canvasWidth = canvas.getWidth(); const canvasHeight = canvas.getHeight(); let dx = 0, dy = 0; if (rect.left < 0) { dx = -rect.left; } else if (rect.left + rect.width > canvasWidth) { dx = canvasWidth - (rect.left + rect.width); } if (rect.top < 0) { dy = -rect.top; } else if (rect.top + rect.height > canvasHeight) { dy = canvasHeight - (rect.top + rect.height); } if (dx !== 0 || dy !== 0) { const originX = selectedObject.originX || "center"; const originY = selectedObject.originY || "center"; const newCenter = new fabric.Point( ((_a2 = selectedObject.left) != null ? _a2 : 0) + dx, ((_b2 = selectedObject.top) != null ? _b2 : 0) + dy ); selectedObject.setPositionByOrigin(newCenter, originX, originY); selectedObject.setCoords(); } } canvas.renderAll(); setProperties((prev) => ({ ...prev, ...updatedProperties })); }); }; return { updateObject }; }; // src/utils/index.ts var toFloat = (num) => { if (typeof num !== "number" || isNaN(num)) return ""; return num % 1 !== 0 ? Number(num.toFixed(2)) : num; }; var angleOptions = [ { value: 45, label: "45\xB0" }, { value: 90, label: "90\xB0" }, { value: 180, label: "180\xB0" }, { value: 270, label: "270\xB0" } ]; var CommonProperties = ({ properties, updateObject }) => { var _a, _b; return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-gray-600", children: "Position X" }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [ /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.left === "number") { updateObject({ left: properties.left - 1 }); } }, disabled: typeof properties.left !== "number", children: "-" } ), /* @__PURE__ */ jsx( "input", { type: "number", value: toFloat(properties.left), onChange: (e) => updateObject({ left: Number(e.target.value) }), className: "w-16 rounded border border-solid border-gray-200 bg-white px-1 py-0.5 text-center text-xs [appearance:textfield] focus:outline-none focus:ring-1 focus:ring-gray-500 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" } ), /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.left === "number") { updateObject({ left: properties.left + 1 }); } }, disabled: typeof properties.left !== "number", children: "+" } ) ] }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-gray-600", children: "Position Y" }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [ /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.top === "number") { updateObject({ top: properties.top - 1 }); } }, disabled: typeof properties.top !== "number", children: "-" } ), /* @__PURE__ */ jsx( "input", { type: "number", value: toFloat(properties.top), onChange: (e) => updateObject({ top: Number(e.target.value) }), className: "w-16 rounded border border-solid border-gray-200 bg-white px-1 py-0.5 text-center text-xs [appearance:textfield] focus:outline-none focus:ring-1 focus:ring-gray-500 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" } ), /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.top === "number") { updateObject({ top: properties.top + 1 }); } }, disabled: typeof properties.top !== "number", children: "+" } ) ] }) ] }), properties.type !== "circle" && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-gray-600", children: "Width" }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [ /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.width === "number") { updateObject({ width: properties.width - 1 }); } }, disabled: typeof properties.width !== "number", children: "-" } ), /* @__PURE__ */ jsx( "input", { type: "number", value: toFloat((_a = properties.width) != null ? _a : 0), onChange: (e) => updateObject({ width: Number(e.target.value) }), className: "w-16 rounded border border-solid border-gray-200 bg-white px-1 py-0.5 text-center text-xs [appearance:textfield] focus:outline-none focus:ring-1 focus:ring-gray-500 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" } ), /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.width === "number") { updateObject({ width: properties.width + 1 }); } }, disabled: typeof properties.width !== "number", children: "+" } ) ] }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-gray-600", children: "Height" }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [ /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.height === "number") { updateObject({ height: properties.height - 1 }); } }, disabled: typeof properties.height !== "number", children: "-" } ), /* @__PURE__ */ jsx( "input", { type: "number", value: toFloat((_b = properties.height) != null ? _b : 0), onChange: (e) => updateObject({ height: Number(e.target.value) }), className: "w-16 rounded border border-solid border-gray-200 bg-white px-1 py-0.5 text-center text-xs [appearance:textfield] focus:outline-none focus:ring-1 focus:ring-gray-500 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" } ), /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.height === "number") { updateObject({ height: properties.height + 1 }); } }, disabled: typeof properties.height !== "number", children: "+" } ) ] }) ] }) ] }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-gray-600", children: "Angle" }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [ /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center rounded border border-solid border-gray-200 text-xs transition-colors hover:bg-gray-100", onClick: () => { if (typeof properties.angle === "number") { updateObject({ angle: properties.angle - 1 }); } }, disabled: typeof properties.angle !== "number", children: "-" } ), /* @__PURE__ */ jsx( "input", { type: "number", value: toFloat(properties.angle), onChange: (e) => updateObject({ angle: Number(e.target.value) }), className: "w-16 rounded border border-solid border-gray-200 bg-white px-1 py-0.5 text-center text-xs [appearance:textfield] focus:outline-none focus:ring-1 focus:ring-gray-500 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"