figmatic-print-builder/Editor.jsx

555 lines
21 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}