figmatic-print-builder/PropertiesPanel.jsx

828 lines
34 KiB
React
Raw Permalink Normal View History

2026-06-16 12:58:18 +05:30
import useEditorStore from "./useEditorStore";
import { useState } from "react";
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
function SortableColumnItem({ column, elementId, updateColumnWidth, updateColumnLabel, toggleColumnVisibility }) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: column.key });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={{ ...styles.columnItem, ...style }} {...attributes}>
<div style={{ display: "flex", alignItems: "center", gap: 8, flex: 1 }}>
<div {...listeners} style={{ cursor: "grab", padding: "0 4px" }}>
</div>
<label style={styles.checkbox}>
<input
type="checkbox"
checked={column.visible}
onChange={() => toggleColumnVisibility(elementId, column.key)}
/>
<span>{column.key}</span>
</label>
</div>
<div style={{ display: "flex", gap: 4 }}>
<input
type="text"
value={column.label}
onChange={(e) => updateColumnLabel(elementId, column.key, e.target.value)}
style={{ ...styles.input, width: 70, fontSize: 11 }}
placeholder="Label"
/>
<input
type="number"
value={column.width}
onChange={(e) => updateColumnWidth(elementId, column.key, Number(e.target.value))}
style={{ ...styles.input, width: 50, fontSize: 11 }}
placeholder="W"
/>
</div>
</div>
);
}
export default function PropertiesPanel() {
const elements = useEditorStore((state) => state.elements);
const selectedId = useEditorStore((state) => state.selectedId);
const updateElement = useEditorStore((state) => state.updateElement);
const duplicateElement = useEditorStore((state) => state.duplicateElement);
const deleteElement = useEditorStore((state) => state.deleteElement);
const toggleColumnVisibility = useEditorStore((state) => state.toggleColumnVisibility);
const updateColumnWidth = useEditorStore((state) => state.updateColumnWidth);
const updateColumnLabel = useEditorStore((state) => state.updateColumnLabel);
const reorderColumns = useEditorStore((state) => state.reorderColumns);
const selected = elements.find((el) => el.id === selectedId);
const [imageUpload, setImageUpload] = useState(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
if (!selected) {
return (
<div style={styles.panel}>
<div style={styles.emptyState}>
<p>Select an element to edit properties</p>
</div>
</div>
);
}
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
updateElement(selected.id, { src: event.target.result });
};
reader.readAsDataURL(file);
}
};
const handleDragEnd = (event) => {
const { active, over } = event;
if (active.id !== over.id) {
const oldIndex = selected.columns.findIndex((col) => col.key === active.id);
const newIndex = selected.columns.findIndex((col) => col.key === over.id);
const newColumns = arrayMove(selected.columns, oldIndex, newIndex);
reorderColumns(selected.id, newColumns);
}
};
return (
<div style={styles.panel}>
<div style={styles.header}>
<h4 style={styles.title}>Properties</h4>
<div style={styles.actions}>
<button
onClick={() => duplicateElement(selected.id)}
style={styles.iconBtn}
title="Duplicate"
>
📋
</button>
<button
onClick={() => deleteElement(selected.id)}
style={styles.iconBtn}
title="Delete"
>
🗑
</button>
</div>
</div>
<div style={styles.section}>
<label style={styles.label}>Type</label>
<div style={styles.badge}>{selected.type.toUpperCase()}</div>
</div>
{/* POSITION */}
<div style={styles.section}>
<label style={styles.label}>Position</label>
<div style={styles.row}>
<div style={styles.inputGroup}>
<span style={styles.inputLabel}>X</span>
<input
type="number"
value={selected.x}
onChange={(e) =>
updateElement(selected.id, { x: Number(e.target.value) })
}
style={styles.input}
/>
</div>
<div style={styles.inputGroup}>
<span style={styles.inputLabel}>Y</span>
<input
type="number"
value={selected.y}
onChange={(e) =>
updateElement(selected.id, { y: Number(e.target.value) })
}
style={styles.input}
/>
</div>
</div>
</div>
{/* ROTATION */}
{selected.rotation !== undefined && (
<div style={styles.section}>
<label style={styles.label}>Rotation</label>
<input
type="number"
value={Math.round(selected.rotation)}
onChange={(e) =>
updateElement(selected.id, { rotation: Number(e.target.value) })
}
style={styles.input}
placeholder="Degrees"
/>
</div>
)}
{/* TEXT PROPERTIES */}
{selected.type === "text" && (
<>
<div style={styles.section}>
<label style={styles.label}>Text</label>
<textarea
value={selected.text}
onChange={(e) =>
updateElement(selected.id, { text: e.target.value })
}
style={{ ...styles.input, minHeight: 60, resize: "vertical" }}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Font Size</label>
<input
type="number"
value={selected.fontSize}
onChange={(e) =>
updateElement(selected.id, {
fontSize: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Font Family</label>
<select
value={selected.fontFamily}
onChange={(e) =>
updateElement(selected.id, { fontFamily: e.target.value })
}
style={styles.input}
>
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
<option value="Georgia">Georgia</option>
<option value="Verdana">Verdana</option>
</select>
</div>
<div style={styles.section}>
<label style={styles.label}>Font Weight</label>
<select
value={selected.fontWeight || "normal"}
onChange={(e) =>
updateElement(selected.id, { fontWeight: e.target.value })
}
style={styles.input}
>
<option value="normal">Normal (400)</option>
<option value="500">Medium (500)</option>
<option value="600">Semi Bold (600)</option>
<option value="bold">Bold (700)</option>
<option value="800">Extra Bold (800)</option>
<option value="900">Black (900)</option>
</select>
</div>
<div style={styles.section}>
<label style={styles.label}>Color</label>
<input
type="color"
value={selected.color}
onChange={(e) =>
updateElement(selected.id, { color: e.target.value })
}
style={{ ...styles.input, height: 40 }}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Style</label>
<div style={styles.row}>
<label style={styles.checkbox}>
<input
type="checkbox"
checked={selected.bold}
onChange={(e) =>
updateElement(selected.id, { bold: e.target.checked })
}
/>
<span>Bold</span>
</label>
<label style={styles.checkbox}>
<input
type="checkbox"
checked={selected.italic}
onChange={(e) =>
updateElement(selected.id, { italic: e.target.checked })
}
/>
<span>Italic</span>
</label>
</div>
<div style={{ marginTop: 8 }}>
<label style={styles.checkbox}>
<input
type="checkbox"
checked={selected.underline}
onChange={(e) =>
updateElement(selected.id, { underline: e.target.checked })
}
/>
<span>Underline</span>
</label>
</div>
</div>
<div style={styles.section}>
<label style={styles.label}>Alignment</label>
<select
value={selected.align}
onChange={(e) =>
updateElement(selected.id, { align: e.target.value })
}
style={styles.input}
>
<option value="left">Left</option>
<option value="center">Center</option>
<option value="right">Right</option>
</select>
</div>
</>
)}
{/* IMAGE PROPERTIES */}
{selected.type === "image" && (
<>
<div style={styles.section}>
<label style={styles.label}>Image</label>
{selected.src ? (
<div style={{ position: "relative" }}>
<img
src={selected.src}
alt="Preview"
style={{
width: "100%",
height: 120,
objectFit: "cover",
borderRadius: 4,
border: "1px solid #ddd",
}}
/>
<button
onClick={() => updateElement(selected.id, { src: null })}
style={{
position: "absolute",
top: 8,
right: 8,
background: "#ff4444",
color: "#fff",
border: "none",
borderRadius: 4,
padding: "4px 8px",
cursor: "pointer",
fontSize: 12,
}}
>
Delete
</button>
</div>
) : (
<input
type="file"
accept="image/*"
onChange={handleImageUpload}
style={styles.input}
/>
)}
</div>
<div style={styles.section}>
<label style={styles.label}>Size</label>
<div style={styles.row}>
<div style={styles.inputGroup}>
<span style={styles.inputLabel}>W</span>
<input
type="number"
value={selected.width}
onChange={(e) =>
updateElement(selected.id, {
width: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.inputGroup}>
<span style={styles.inputLabel}>H</span>
<input
type="number"
value={selected.height}
onChange={(e) =>
updateElement(selected.id, {
height: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
</div>
</div>
</>
)}
{/* QRCODE PROPERTIES */}
{selected.type === "qrcode" && (
<>
<div style={styles.section}>
<label style={styles.label}>QR Code Data</label>
<textarea
value={selected.data}
onChange={(e) =>
updateElement(selected.id, { data: e.target.value })
}
style={{ ...styles.input, minHeight: 60, resize: "vertical" }}
placeholder="Enter URL or text"
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Size</label>
<input
type="number"
value={selected.size}
onChange={(e) =>
updateElement(selected.id, {
size: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
</>
)}
{/* LINE PROPERTIES */}
{selected.type === "line" && (
<>
<div style={styles.section}>
<label style={styles.label}>Width</label>
<input
type="number"
value={selected.width}
onChange={(e) =>
updateElement(selected.id, {
width: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Thickness</label>
<input
type="number"
value={selected.thickness}
onChange={(e) =>
updateElement(selected.id, {
thickness: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Color</label>
<input
type="color"
value={selected.color}
onChange={(e) =>
updateElement(selected.id, { color: e.target.value })
}
style={{ ...styles.input, height: 40 }}
/>
</div>
</>
)}
{/* RECTANGLE PROPERTIES */}
{selected.type === "rectangle" && (
<>
<div style={styles.section}>
<label style={styles.label}>Size</label>
<div style={styles.row}>
<div style={styles.inputGroup}>
<span style={styles.inputLabel}>W</span>
<input
type="number"
value={selected.width}
onChange={(e) =>
updateElement(selected.id, {
width: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.inputGroup}>
<span style={styles.inputLabel}>H</span>
<input
type="number"
value={selected.height}
onChange={(e) =>
updateElement(selected.id, {
height: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
</div>
</div>
<div style={styles.section}>
<label style={styles.label}>Fill Color</label>
<input
type="color"
value={selected.fill === "transparent" ? "#ffffff" : selected.fill}
onChange={(e) =>
updateElement(selected.id, { fill: e.target.value })
}
style={{ ...styles.input, height: 40 }}
/>
<label style={{ ...styles.checkbox, marginTop: 8 }}>
<input
type="checkbox"
checked={selected.fill === "transparent"}
onChange={(e) =>
updateElement(selected.id, {
fill: e.target.checked ? "transparent" : "#ffffff",
})
}
/>
<span>Transparent</span>
</label>
</div>
<div style={styles.section}>
<label style={styles.label}>Border Color</label>
<input
type="color"
value={selected.stroke}
onChange={(e) =>
updateElement(selected.id, { stroke: e.target.value })
}
style={{ ...styles.input, height: 40 }}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Border Width</label>
<input
type="number"
value={selected.strokeWidth}
onChange={(e) =>
updateElement(selected.id, {
strokeWidth: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Corner Radius</label>
<input
type="number"
value={selected.cornerRadius}
onChange={(e) =>
updateElement(selected.id, {
cornerRadius: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
</>
)}
{/* SUMMARY PROPERTIES */}
{selected.type === "summary" && (
<>
<div style={styles.section}>
<label style={styles.label}>Font Size</label>
<input
type="number"
value={selected.fontSize}
onChange={(e) =>
updateElement(selected.id, {
fontSize: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Spacing</label>
<input
type="number"
value={selected.spacing}
onChange={(e) =>
updateElement(selected.id, {
spacing: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Label Color</label>
<input
type="color"
value={selected.labelColor}
onChange={(e) =>
updateElement(selected.id, { labelColor: e.target.value })
}
style={{ ...styles.input, height: 40 }}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Value Color</label>
<input
type="color"
value={selected.valueColor}
onChange={(e) =>
updateElement(selected.id, { valueColor: e.target.value })
}
style={{ ...styles.input, height: 40 }}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Fields</label>
{selected.fields.map((field, idx) => (
<div key={field.key} style={styles.columnItem}>
<label style={styles.checkbox}>
<input
type="checkbox"
checked={field.visible}
onChange={() => {
const newFields = [...selected.fields];
newFields[idx].visible = !newFields[idx].visible;
updateElement(selected.id, { fields: newFields });
}}
/>
<span>{field.label}</span>
</label>
<input
type="text"
value={field.label}
onChange={(e) => {
const newFields = [...selected.fields];
newFields[idx].label = e.target.value;
updateElement(selected.id, { fields: newFields });
}}
style={{ ...styles.input, width: 80 }}
placeholder="Label"
/>
</div>
))}
</div>
</>
)}
{/* TABLE PROPERTIES */}
{selected.type === "table" && (
<>
<div style={styles.section}>
<label style={styles.label}>Row Height</label>
<input
type="number"
value={selected.rowHeight}
onChange={(e) =>
updateElement(selected.id, {
rowHeight: Number(e.target.value),
})
}
style={styles.input}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Columns (Drag to reorder)</label>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={selected.columns.map((col) => col.key)}
strategy={verticalListSortingStrategy}
>
{selected.columns.map((col) => (
<SortableColumnItem
key={col.key}
column={col}
elementId={selected.id}
updateColumnWidth={updateColumnWidth}
updateColumnLabel={updateColumnLabel}
toggleColumnVisibility={toggleColumnVisibility}
/>
))}
</SortableContext>
</DndContext>
</div>
<div style={styles.section}>
<label style={styles.label}>Header Background</label>
<input
type="color"
value={selected.headerBg}
onChange={(e) =>
updateElement(selected.id, { headerBg: e.target.value })
}
style={{ ...styles.input, height: 40 }}
/>
</div>
<div style={styles.section}>
<label style={styles.label}>Border Color</label>
<input
type="color"
value={selected.borderColor}
onChange={(e) =>
updateElement(selected.id, { borderColor: e.target.value })
}
style={{ ...styles.input, height: 40 }}
/>
</div>
<div style={styles.section}>
<label style={styles.checkbox}>
<input
type="checkbox"
checked={selected.showSampleData}
onChange={(e) =>
updateElement(selected.id, { showSampleData: e.target.checked })
}
/>
<span>Show Sample Data</span>
</label>
</div>
</>
)}
</div>
);
}
const styles = {
panel: {
width: 280,
background: "#fff",
borderLeft: "1px solid #ddd",
padding: 16,
overflowY: "auto",
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
},
title: {
margin: 0,
fontSize: 16,
fontWeight: 600,
},
actions: {
display: "flex",
gap: 4,
},
iconBtn: {
background: "none",
border: "1px solid #ddd",
borderRadius: 4,
padding: "4px 8px",
cursor: "pointer",
fontSize: 14,
},
emptyState: {
textAlign: "center",
color: "#999",
marginTop: 40,
},
section: {
marginBottom: 16,
},
label: {
display: "block",
fontSize: 12,
fontWeight: 600,
marginBottom: 6,
color: "#333",
},
input: {
width: "100%",
padding: 8,
border: "1px solid #ddd",
borderRadius: 4,
fontSize: 13,
boxSizing: "border-box",
},
row: {
display: "flex",
gap: 8,
},
inputGroup: {
flex: 1,
display: "flex",
flexDirection: "column",
},
inputLabel: {
fontSize: 11,
color: "#666",
marginBottom: 4,
},
checkbox: {
display: "flex",
alignItems: "center",
gap: 6,
fontSize: 13,
cursor: "pointer",
},
badge: {
display: "inline-block",
padding: "4px 8px",
background: "#e3f2fd",
color: "#1976d2",
borderRadius: 4,
fontSize: 11,
fontWeight: 600,
},
columnItem: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "6px 0",
borderBottom: "1px solid #f0f0f0",
},
};