import React, { useState, useEffect, useRef, useMemo, createContext, useContext } from 'react';
import { BrowserRouter, Routes, Route, useNavigate, Link } from 'react-router-dom';
import { Stage, Layer, Text, Image as KonvaImage, Transformer, Rect, Group } from 'react-konva';
import axios from 'axios';
import {
Type, Image as ImageIcon, Settings, Trash2, Undo, Redo,
ShoppingBag, Truck, LayoutDashboard, MessageCircle, ChevronRight,
ArrowRight, Paintbrush, ShieldCheck, CheckCircle, Plus, Minus,
Layers, Star, TrendingUp, Award, Save, X, Menu, Zap, Box, Fingerprint
} from 'lucide-react';
const API_URL = 'http://localhost:5000/api';
// ==========================================
// UTILS & ERROR HANDLING
// ==========================================
class ErrorBoundary extends React.Component {
constructor(props) { super(props); this.state = { hasError: false }; }
static getDerivedStateFromError() { return { hasError: true }; }
render() {
if (this.state.hasError) {
return (
STUDIO ERROR
The design engine encountered a conflict. We've notified our technicians.
window.location.reload()} className="bg-black text-white px-10 py-4 rounded-full font-bold uppercase tracking-widest text-xs">Restart Session
);
}
return this.props.children;
}
}
const formatRs = (amount) => `Rs. ${Number(amount || 0).toLocaleString('en-LK', { minimumFractionDigits: 2 })}`;
const DEFAULT_SETTINGS = {
basePrices: { budget: 1500, corporate: 2500, premium: 3500 },
printFees: { front: 800, back: 800, left: 400, right: 400 },
rushFee: 1500, deliveryFee: 500, whatsappNumber: "94741336159", bankDetails: "Bank: Commercial Bank\nAccount: 0987654321",
discountTiers: [{ minQty: 15, discountPercent: 5 }, { minQty: 50, discountPercent: 10 }]
};
const SHIRT_COLORS = [
{ name: 'Pure White', hex: '#ffffff', stroke: '#e2e8f0' },
{ name: 'Onyx Black', hex: '#111827', stroke: '#000000' },
{ name: 'Navy Blue', hex: '#1e3a8a', stroke: '#172554' },
{ name: 'Heather Gray', hex: '#94a3b8', stroke: '#64748b' },
];
const SIZES = ['S', 'M', 'L', 'XL', '2XL', '3XL'];
const SettingsContext = createContext(DEFAULT_SETTINGS);
const AppProvider = ({ children }) => {
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
useEffect(() => {
axios.get(`${API_URL}/settings`)
.then(res => { if (res.data?.basePrices) setSettings(res.data); })
.catch(() => console.log("DesignLab: Using local defaults"));
}, []);
return {children} ;
};
// ==========================================
// CANVAS ENGINE COMPONENTS
// ==========================================
const TShirtBackground = ({ color, stroke, view }) => {
let path = "M25 10 Q 50 25 75 10 L 95 30 L 80 45 L 80 95 L 20 95 L 20 45 L 5 30 Z";
if (view === 'left' || view === 'right') path = "M30 10 Q 50 5 70 10 L 85 40 L 70 95 L 30 95 L 15 40 Z";
return (
);
};
const KonvaNodeImage = ({ node, onSelect, isReadOnly }) => {
const [image, setImage] = useState(null);
useEffect(() => {
if (!node.url) return;
const img = new window.Image();
img.src = node.url;
img.crossOrigin = 'Anonymous';
img.onload = () => setImage(img);
}, [node.url]);
return (
);
};
// ==========================================
// VIEW: PREMIUM HOMEPAGE
// ==========================================
const LandingPage = () => (
Start Project
Precision Print Technology
WE BUILD, YOU DESIGN.
The ultimate design lab for modern brands. High-performance fabrics meet an uncompromising mobile editor.
Enter Design Lab
{[
{ icon:
, title: "Identity", desc: "Every order is a unique project. Our editor ensures your brand identity is translated perfectly to fabric." },
{ icon: , title: "Precision", desc: "Automated production scheduling means consistent quality and 48-hour dispatch lead times." },
{ icon: , title: "Logistics", desc: "Real-time tracking and insured delivery across Sri Lanka and international destinations." }
].map((item, i) => (
{item.icon}
{item.title}
{item.desc}
))}
);
// ==========================================
// VIEW: DESIGN LAB
// ==========================================
const DesignLab = () => {
const navigate = useNavigate();
const settings = useContext(SettingsContext);
const containerRef = useRef(null);
const stageRef = useRef(null);
const [nodes, setNodes] = useState({ front: [], back: [], left: [], right: [] });
const [activeView, setActiveView] = useState('front');
const [selectedId, setSelectedId] = useState(null);
const [scale, setScale] = useState(1);
const [config, setConfig] = useState({
material: 'corporate', color: SHIRT_COLORS[0],
sizeQuantities: { 'S': 0, 'M': 1, 'L': 0, 'XL': 0, '2XL': 0 }, rush: false
});
const activeNodes = nodes[activeView] || [];
const selectedNode = activeNodes.find(n => n.id === selectedId);
const totalQty = Object.values(config.sizeQuantities).reduce((a, b) => a + b, 0);
useEffect(() => {
const resizer = () => {
if (!containerRef.current) return;
const sX = (containerRef.current.clientWidth - 40) / 360;
const sY = (containerRef.current.clientHeight - 120) / 480;
setScale(Math.max(Math.min(sX, sY, 1.15), 0.45));
};
window.addEventListener('resize', resizer); setTimeout(resizer, 100);
return () => window.removeEventListener('resize', resizer);
}, []);
const pricing = useMemo(() => {
const baseP = settings.basePrices?.[config.material] || 2500;
let pSideCount = Object.values(nodes).filter(v => v.length > 0).length;
let costPerShirt = baseP + (pSideCount * 800);
let disc = 0;
const tier = (settings.discountTiers || []).sort((a,b)=>b.minQty-a.minQty).find(t => totalQty >= t.minQty);
if (tier) disc = tier.discountPercent;
const finalPerShirt = costPerShirt * (1 - (disc/100));
return { perShirt: finalPerShirt, total: finalPerShirt * totalQty + (config.rush ? 1500 : 0) };
}, [nodes, config, settings, totalQty]);
const addText = () => {
const id = `t_${Date.now()}`;
const n = [...activeNodes, { id, type: 'text', text: 'NEW LAYER', x: 60, y: 150, fontSize: 26, fill: '#000', fontFamily: 'Arial', rotation: 0, scaleX: 1, scaleY: 1 }];
setNodes({...nodes, [activeView]: n}); setSelectedId(id);
};
const handleFileUpload = (e) => {
const file = e.target.files[0]; if (!file) return;
const url = URL.createObjectURL(file);
const id = `i_${Date.now()}`;
const n = [...activeNodes, { id, type: 'image', url, x: 50, y: 100, width: 120, height: 120, rotation: 0, scaleX: 1, scaleY: 1 }];
setNodes({...nodes, [activeView]: n}); setSelectedId(id);
};
return (
C
Studio
{
if (totalQty < 1) return alert("Please specify quantities in the right panel.");
const orderData = { design: { nodes, config, pricing }, projectJson: { nodes, config } };
localStorage.setItem('cenzi_current_order', JSON.stringify(orderData));
navigate('/checkout');
}} className="bg-black text-white px-8 py-2.5 rounded-full font-black text-[10px] uppercase tracking-widest hover:bg-gray-800 transition-all flex items-center gap-2">
Review Order
{/* Editor Sidebar */}
{selectedNode && (
)}
Material
setConfig({...config, material: e.target.value})} className="w-full p-4 bg-gray-50 rounded-2xl font-black text-[10px] uppercase tracking-widest mb-6 outline-none border-none shadow-sm">
Budget Poly
Corporate Cotton
Heavyweight Luxury
{SHIRT_COLORS.map(c => (
setConfig({...config, color: c})} className={`w-9 h-9 rounded-full border-2 ${config.color.hex === c.hex ? 'border-black ring-4 ring-gray-100 scale-110' : 'border-transparent shadow-inner'}`} style={{background: c.hex}} />
))}
{/* Workspace */}
{if(e.target.name()==='bg') setSelectedId(null)}}>
{activeNodes.map(n => n.type === 'text' ? (
setSelectedId(n.id)} onDragEnd={e => {
const u = activeNodes.map(nd => nd.id === n.id ? {...nd, x: e.target.x(), y: e.target.y()} : nd);
setNodes({...nodes, [activeView]: u});
}} />
) : (
setSelectedId(n.id)} isReadOnly={false} />
))}
{selectedId && }
{['front', 'back', 'left', 'right'].map(v => (
{ setActiveView(v); setSelectedId(null); }} className={`px-7 py-2.5 rounded-full text-[9px] font-black uppercase tracking-[0.2em] transition-all ${activeView === v ? 'bg-white text-black' : 'text-gray-500 hover:text-white'}`}>
{v} {nodes[v].length > 0 &&
}
))}
{/* Production Sidebar */}
);
};
// ==========================================
// VIEW: CHECKOUT
// ==========================================
const CheckoutView = () => {
const navigate = useNavigate();
const settings = useContext(SettingsContext);
const [orderData, setOrderData] = useState(null);
const [dbOrder, setDbOrder] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const data = localStorage.getItem('cenzi_current_order');
if (data) setOrderData(JSON.parse(data)); else navigate('/design');
}, [navigate]);
const handlePlaceOrder = async (e) => {
e.preventDefault(); setLoading(true);
const fd = new FormData(e.target);
const payload = {
customer: { name: fd.get('name'), phone: fd.get('phone'), deliveryMethod: fd.get('delivery'), address: fd.get('address') || 'Store Pickup' },
design: { color: orderData.design.config.color.name, sizeQuantities: orderData.design.config.sizeQuantities, style: orderData.design.config.style, material: orderData.design.config.material },
pricing: orderData.design.pricing, projectJson: orderData.projectJson
};
try {
const res = await axios.post(`${API_URL}/orders`, payload);
setDbOrder(res.data); localStorage.removeItem('cenzi_current_order');
} catch(err) { alert("Production server is currently offline. Please try again in 5 minutes."); }
finally { setLoading(false); }
};
if (!orderData) return null;
return (
);
};
// ==========================================
// VIEW: ADMIN (ISOLATED)
// ==========================================
const AdminLayout = () => {
const [token, setToken] = useState(localStorage.getItem('cenzi_admin_token'));
const [orders, setOrders] = useState([]);
useEffect(() => {
if (token) axios.get(`${API_URL}/orders`, { headers: { Authorization: `Bearer ${token}` } }).then(res => setOrders(res.data)).catch(() => setToken(null));
}, [token]);
if (!token) return (
{ e.preventDefault(); try { const res = await axios.post(`${API_URL}/auth/login`, { password: e.target.pw.value }); setToken(res.data.token); localStorage.setItem('cenzi_admin_token', res.data.token); } catch(err) { alert("Access Denied"); } }} className="bg-white p-12 rounded-[2.5rem] max-w-sm w-full shadow-2xl">
Admin Port
AUTHENTICATE
);
return (
CENZI.
Pipeline
Analytics
{ setToken(null); localStorage.removeItem('cenzi_admin_token'); }} className="text-red-400 font-black text-[10px] tracking-[0.2em] uppercase text-left mt-10">Termination
Queue Real-time production monitoring
Reference Agent Value Priority
{orders.map(o => (
{o.orderId}
{o.customer.name}
{o.customer.phone}
{formatRs(o.pricing?.total)}
{o.status}
))}
{orders.length === 0 &&
No active tickets found.
}
);
};
// ==========================================
// MASTER ROUTER
// ==========================================
export default function App() {
return (
} />
} />
} />
} />
);
}