555 lines
21 KiB
JavaScript
555 lines
21 KiB
JavaScript
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>
|
||
);
|
||
} |