figmatic-print-builder/Editor.jsx

555 lines
21 KiB
React
Raw Permalink Normal View History

2026-06-16 12:58:18 +05:30
import { useEffect, useState } from "react";
import useEditorStore from "./useEditorStore.js";
import CanvasStage from "./CanvasStage";
import Toolbox from "./Toolbox";
import PropertiesPanel from "./PropertiesPanel";
import LayersPanel from "./LayersPanel";
import { PAGE_SIZES } from "./utils/pageSizes";
import "./Editor.scss";
import { generateHtml } from "./utils/exportToHtml.js";
import { printHtml } from "./utils/printHtml.js";
export default function Editor() {
const {
undo,
redo,
deleteSelected,
copyElement,
pasteElement,
exportTemplate,
selectedId,
setPageSize,
page,
toggleLayers,
showLayers,
} = useEditorStore();
const [zoom, setZoom] = useState(0.9);
const [selectedPageSize, setSelectedPageSize] = useState("A4");
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e) => {
// Ignore shortcuts when typing in input/textarea
const isTyping =
e.target.tagName === "INPUT" ||
e.target.tagName === "TEXTAREA" ||
e.target.isContentEditable;
if (isTyping) return;
// Ctrl/Cmd + Z = Undo
if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
e.preventDefault();
undo();
}
// Ctrl/Cmd + Shift + Z = Redo
if ((e.ctrlKey || e.metaKey) && e.key === "z" && e.shiftKey) {
e.preventDefault();
redo();
}
// Ctrl/Cmd + Y = Redo (alternative)
if ((e.ctrlKey || e.metaKey) && e.key === "y") {
e.preventDefault();
redo();
}
// Delete/Backspace = Delete selected (only when not typing)
if ((e.key === "Delete" || e.key === "Backspace") && selectedId) {
e.preventDefault();
deleteSelected();
}
// Ctrl/Cmd + C = Copy
if ((e.ctrlKey || e.metaKey) && e.key === "c" && selectedId) {
e.preventDefault();
copyElement();
}
// Ctrl/Cmd + V = Paste
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
e.preventDefault();
pasteElement();
}
// Ctrl/Cmd + D = Duplicate
if ((e.ctrlKey || e.metaKey) && e.key === "d" && selectedId) {
e.preventDefault();
const { duplicateElement } = useEditorStore.getState();
duplicateElement(selectedId);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [undo, redo, deleteSelected, copyElement, pasteElement, selectedId]);
const handleSave = () => {
debugger
const json = exportTemplate();
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "invoice-template.json";
a.click();
URL.revokeObjectURL(url);
};
const handlePageSizeChange = (e) => {
const sizeName = e.target.value;
setSelectedPageSize(sizeName);
const size = PAGE_SIZES[sizeName];
setPageSize({ ...size, name: sizeName });
};
const handleZoomIn = () => {
setZoom((prev) => Math.min(prev + 0.1, 2));
};
const handleZoomOut = () => {
setZoom((prev) => Math.max(prev - 0.1, 0.3));
};
const handleLoadTestTemplate = async () => {
const testTemplate = {
"elements": [
{
"id": "73079a52-87ac-4bd8-a10e-b901e316a911",
"type": "text",
"x": 30,
"y": 40,
"text": "Pozomind Technologies",
"fontSize": 25,
"fontFamily": "Arial",
"fontWeight": "normal",
"color": "#ff0000",
"align": "left",
"bold": true,
"italic": false,
"underline": true,
"rotation": 0
},
{
"id": "af702c6a-03b0-498b-8511-3737b754c15d",
"type": "image",
"x": 630,
"y": 20,
"width": 150,
"height": 100,
"src": "http://192.168.1.38/upload/getfile?fileId=69cbba1bdb04001678b283b8.png",
"rotation": 0
},
{
"id": "58ae6bb8-039c-46f3-b5de-6000553a89cb",
"type": "text",
"x": 30,
"y": 80,
"text": "51, Aahhaa Restaurant Back Side, Amirtha Nagar,\nStep Colony, Dharga, Hosur, Krishnagiri,\nTamil Nadu - 635126",
"fontSize": 18,
"fontFamily": "Arial",
"fontWeight": "normal",
"color": "#000000",
"align": "left",
"bold": false,
"italic": false,
"underline": false,
"rotation": 0
},
{
"id": "969bda69-023b-461e-9c65-7842004d1289",
"type": "table",
"x": 30,
"y": 210,
"width": 480,
"columns": [
{
"key": "sno",
"label": "S.No",
"width": 50,
"visible": true
},
{
"key": "item",
"label": "Item",
"width": 150,
"visible": true
},
{
"key": "hsn",
"label": "HSN",
"width": 60,
"visible": true
},
{
"key": "qty",
"label": "Qty",
"width": 50,
"visible": true
},
{
"key": "mrp",
"label": "MRP",
"width": 60,
"visible": true
},
{
"key": "rate",
"label": "Rate",
"width": 60,
"visible": true
},
{
"key": "dis",
"label": "Dis",
"width": 50,
"visible": true
},
{
"key": "tax",
"label": "Tax %",
"width": 60,
"visible": true
},
{
"key": "amt",
"label": "Amt",
"width": 70,
"visible": true
}
],
"sampleData": [
{
"sno": "1",
"item": "ITEM 1",
"hsn": "01",
"qty": "2",
"mrp": "100",
"rate": "88",
"dis": "0",
"tax": "5",
"amt": "176"
},
{
"sno": "2",
"item": "ITEM 2",
"hsn": "02",
"qty": "1",
"mrp": "350",
"rate": "300",
"dis": "0",
"tax": "5",
"amt": "300"
},
{
"sno": "3",
"item": "ITEM 3",
"hsn": "03",
"qty": "1",
"mrp": "250",
"rate": "200",
"dis": "0",
"tax": "0",
"amt": "200"
},
{
"sno": "4",
"item": "ITEM 4",
"hsn": "04",
"qty": "1",
"mrp": "50",
"rate": "30",
"dis": "0",
"tax": "18",
"amt": "30"
},
{
"sno": "5",
"item": "ITEM 5",
"hsn": "05",
"qty": "1",
"mrp": "50",
"rate": "30",
"dis": "0",
"tax": "40",
"amt": "30"
}
],
"rowHeight": 30,
"headerBg": "#f0f0f0",
"borderColor": "#000000",
"showSampleData": true,
"rotation": 0
},
{
"id": "098a7eac-5660-40d7-91b2-d7df56fccfd4",
"type": "text",
"x": 360,
"y": 170,
"text": "Takeaway",
"fontSize": 18,
"fontFamily": "Arial",
"fontWeight": "normal",
"color": "#000000",
"align": "left",
"bold": false,
"italic": false,
"underline": false,
"rotation": 0
},
{
"id": "dcfcd7b5-965c-4aa7-bfa6-9bba2dd1f147",
"type": "qrcode",
"x": 670,
"y": 450,
"size": 100,
"data": "https://example.com",
"rotation": 0
},
{
"id": "a2812eeb-f2fa-44df-a4a9-cd70b69a0115",
"type": "summary",
"x": 30,
"y": 460,
"fields": [
{
"key": "items",
"label": "Items",
"visible": true,
"width": 150
},
{
"key": "qty",
"label": "Qty",
"visible": true,
"width": 100
},
{
"key": "amount",
"label": "Amount",
"visible": true,
"width": 150
}
],
"fontSize": 14,
"labelColor": "#000000",
"valueColor": "#000000",
"spacing": 10,
"rotation": 0
},
{
"id": "6a4de268-9839-4ee6-b612-c6b4669830ea",
"type": "line",
"x": 40.82365943816583,
"y": 150.00000000000014,
"width": 729.4637752663348,
"thickness": 2,
"color": "#000000",
"rotation": 0
}
],
"page": {
"width": 794,
"height": 1123,
"name": "A4"
}
};
const { importTemplate } = useEditorStore.getState();
importTemplate(JSON.stringify(testTemplate));
const data = {
companyName: "Pozomind Technologies",
address: "123 Main St",
items: "10",
qty: "25",
amount: "₹5000",
tableData: [
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
{ sno: "1", item: "Product A", qty: "2", amt: "200" },
]
};
const html = generateHtml(testTemplate, data);
await printHtml(html);
};
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column", width: '83vw', fontFamily: 'Poppins, sans-serif' }}>
{/* TOP BAR */}
{/* BODY */}
<div style={{ flex: 1, display: "flex" }}>
{/* LAYERS PANEL */}
{showLayers && <LayersPanel />}
{/* LEFT TOOLBAR */}
<Toolbox />
{/* CANVAS */}
<div style={{
flex: 1,
background: "#f3f3f3",
display: "flex",
flexDirection: "column",
overflow: "auto"
}}>
{/* Page Size Selector */}
<div style={{
padding: "12px 16px",
background: "#fff",
borderBottom: "1px solid #ddd",
display: "flex",
alignItems: "center",
gap: "12px"
}}>
<label style={{ fontSize: 13, fontWeight: 600 }}>Page Size:</label>
<select
value={selectedPageSize}
onChange={handlePageSizeChange}
style={{
padding: "6px 12px",
border: "1px solid #ddd",
borderRadius: 4,
fontSize: 13
}}
>
<option value="A4">A4 (210 × 297 mm)</option>
<option value="A5">A5 (148 × 210 mm)</option>
<option value="THERMAL_2IN">2 inch Thermal</option>
<option value="THERMAL_3IN">3 inch Thermal</option>
</select>
<div style={{ marginLeft: "auto", display: "flex", gap: 8, alignItems: "center" }}>
<button
onClick={handleZoomOut}
style={{
padding: "6px 12px",
border: "1px solid #ddd",
borderRadius: 4,
background: "#fff",
cursor: "pointer",
fontSize: 16
}}
title="Zoom Out"
>
</button>
{/* <span
onClick={handleZoomReset}
style={{
fontSize: 13,
fontWeight: 600,
minWidth: 50,
textAlign: "center",
cursor: "pointer",
userSelect: "none"
}}
title="Reset Zoom"
>
{Math.round(zoom * 100)}%
</span> */}
<button
onClick={handleZoomIn}
style={{
padding: "6px 12px",
border: "1px solid #ddd",
borderRadius: 4,
background: "#fff",
cursor: "pointer",
fontSize: 16
}}
title="Zoom In"
>
+
</button>
</div>
</div>
{/* Canvas Area */}
<div style={{
flex: 1,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: 20
}}>
<div style={{
transform: `scale(${zoom})`,
transformOrigin: "center center",
transition: "transform 0.2s ease"
}}>
<div style={{
background: "#fff",
boxShadow: "0 4px 20px rgba(0,0,0,0.1)"
}}>
<CanvasStage />
</div>
</div>
</div>
</div>
{/* RIGHT PANEL */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '35px' }}>
<PropertiesPanel />
<div style={{
background: "#fff",
borderBottom: "1px solid #ddd",
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
padding: "16px",
width: '280px'
}}>
<div style={{
display: "flex", gap: "8px", flexWrap: "wrap", alignItems: "center", justifyContent: "center", fontFamily: 'Poppins, sans-serif'
}}>
<div className="editor-action-btn secondary" onClick={toggleLayers}>
{showLayers ? "Hide" : "Show"} Layers
</div>
<div className="editor-action-btn secondary" onClick={undo}>
Undo
</div>
<div className="editor-action-btn secondary" onClick={redo}>
Redo
</div>
<div className="editor-action-btn" onClick={handleLoadTestTemplate} style={{ background: "#ff9800", color: "#fff" }}>
🧪 Load Test
</div>
<div
className={`editor-action-btn danger ${!selectedId ? "disabled" : ""}`}
onClick={selectedId ? deleteSelected : undefined}
>
🗑 Delete
</div>
<div className="editor-action-btn">
👁 Preview
</div>
<div className="editor-action-btn primary" onClick={handleSave}>
💾 Save
</div>
</div>
</div>
</div>
</div>
</div>
);
}