seat-picker
Version:
> 📚 [Full Documentation & Live Demo](https://seat-picker-docs.vercel.app/)
1,382 lines (1,377 loc) • 138 kB
JavaScript
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"