/* Canva-style App (Single-file React component) Setup instructions (terminal): 1. Create project: npx create-react-app canva-clone --template cra-template-pwa cd canva-clone 2. Install dependencies: npm install fabric @headlessui/react @heroicons/react 3. Install Tailwind CSS (follow official quickstart): npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p // tailwind.config.js -> content: ["./src/**/*.{js,jsx,ts,tsx}"] // in src/index.css add: @tailwind base; @tailwind components; @tailwind utilities; 4. Replace src/App.jsx with the component below and run: npm start Features included in this single-file demo: - Canvas area using Fabric.js - Add text, upload image, add basic shapes (rect, circle, triangle) - Move/scale/rotate objects - Layer list (basic ordering) - Object styling: color, font size - Undo/redo (simple stack) - Export PNG Notes: This is a focused demo to get a Canva-like editor started. For a production app, you'd add persistence, templates, user auth, better mobile UX, pro features, and optimization. */ import React, { useEffect, useRef, useState } from "react"; import { fabric } from "fabric"; export default function CanvaCloneApp() { const canvasRef = useRef(null); const fabricRef = useRef(null); const [selectedId, setSelectedId] = useState(null); const [layers, setLayers] = useState([]); const [fillColor, setFillColor] = useState("#000000"); const [fontSize, setFontSize] = useState(40); const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); useEffect(() => { const c = new fabric.Canvas("editor", { backgroundColor: "#ffffff", preserveObjectStacking: true, selection: true, width: 1200, height: 700, }); fabricRef.current = c; // update layers on change const refreshLayers = () => { const objs = c.getObjects().map((o, i) => ({ id: o.__uid, name: o.type, index: i, obj: o })); setLayers(objs.slice().reverse()); pushHistory(); }; // assign unique ids let uid = 1; c.on("object:added", (e) => { const obj = e.target; if (!obj.__uid) obj.__uid = `obj_${uid++}`; refreshLayers(); }); c.on("object:removed", refreshLayers); c.on("object:modified", refreshLayers); c.on("selection:created", syncSelection); c.on("selection:updated", syncSelection); c.on("selection:cleared", () => setSelectedId(null)); // initial grid / sample const rect = new fabric.Rect({ left: 50, top: 60, width: 200, height: 120, fill: "#f3f4f6", selectable: false }); c.add(rect); // cleanup canvasRef.current = c; window.addEventListener("resize", handleResize); handleResize(); return () => { window.removeEventListener("resize", handleResize); c.dispose(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Resize to container (simple) const handleResize = () => { const c = fabricRef.current; if (!c) return; // keep fixed size for demo - advanced: scale responsively }; const syncSelection = (e) => { const obj = e.target; setSelectedId(obj ? obj.__uid : null); }; // History (very simple snapshot approach) const pushHistory = () => { const c = fabricRef.current; if (!c) return; try { const json = c.toDatalessJSON(["__uid"]); const next = history.slice(0, historyIndex + 1).concat([json]); setHistory(next); setHistoryIndex(next.length - 1); } catch (err) { console.warn("history push failed", err); } }; const undo = () => { if (historyIndex <= 0) return; const prevIndex = historyIndex - 1; loadFromHistory(prevIndex); }; const redo = () => { if (historyIndex >= history.length - 1) return; const nextIndex = historyIndex + 1; loadFromHistory(nextIndex); }; const loadFromHistory = (index) => { const c = fabricRef.current; if (!c) return; const json = history[index]; c.clear(); c.loadFromJSON(json, () => { c.renderAll(); // reassign any missing ids let uid = 1; c.getObjects().forEach((o) => { if (!o.__uid) o.__uid = `obj_${uid++}`; }); setHistoryIndex(index); const objs = c.getObjects().map((o, i) => ({ id: o.__uid, name: o.type, index: i, obj: o })); setLayers(objs.slice().reverse()); }, function(o, object) { // reviver if needed }); }; // Add text const addText = () => { const c = fabricRef.current; if (!c) return; const text = new fabric.IText("Double-click to edit", { left: 100, top: 100, fill: fillColor, fontSize }); text.__uid = `obj_text_${Date.now()}`; c.add(text).setActiveObject(text); c.renderAll(); pushHistory(); }; // Add shape const addRect = () => { const c = fabricRef.current; const r = new fabric.Rect({ left: 150, top: 150, width: 150, height: 100, fill: fillColor }); r.__uid = `obj_rect_${Date.now()}`; c.add(r).setActiveObject(r); c.renderAll(); pushHistory(); }; const addCircle = () => { const c = fabricRef.current; const o = new fabric.Circle({ left: 200, top: 200, radius: 60, fill: fillColor }); o.__uid = `obj_circle_${Date.now()}`; c.add(o).setActiveObject(o); c.renderAll(); pushHistory(); }; const addTriangle = () => { const c = fabricRef.current; const t = new fabric.Triangle({ left: 240, top: 120, width: 120, height: 120, fill: fillColor }); t.__uid = `obj_tri_${Date.now()}`; c.add(t).setActiveObject(t); c.renderAll(); pushHistory(); }; // Upload image const handleImageUpload = (ev) => { const file = ev.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(f) { const data = f.target.result; fabric.Image.fromURL(data, (img) => { img.set({ left: 120, top: 120, scaleX: 0.6, scaleY: 0.6 }); img.__uid = `obj_img_${Date.now()}`; fabricRef.current.add(img).setActiveObject(img); fabricRef.current.renderAll(); pushHistory(); }); }; reader.readAsDataURL(file); ev.target.value = null; }; // Delete selected const deleteSelected = () => { const c = fabricRef.current; const active = c.getActiveObject(); if (!active) return; if (active.type === "activeSelection") { active.forEachObject((obj) => c.remove(obj)); } else c.remove(active); c.discardActiveObject(); c.renderAll(); pushHistory(); }; // Export const exportPNG = () => { const c = fabricRef.current; const dataURL = c.toDataURL({ format: "png", quality: 1 }); const link = document.createElement("a"); link.href = dataURL; link.download = "design.png"; link.click(); }; // Layer controls const bringForward = (id) => { const c = fabricRef.current; const obj = c.getObjects().find((o) => o.__uid === id); if (obj) { c.bringForward(obj); c.renderAll(); pushHistory(); } }; const sendBack = (id) => { const c = fabricRef.current; const obj = c.getObjects().find((o) => o.__uid === id); if (obj) { c.sendBackwards(obj); c.renderAll(); pushHistory(); } }; const selectLayer = (id) => { const c = fabricRef.current; const obj = c.getObjects().find((o) => o.__uid === id); if (obj) { c.setActiveObject(obj); c.renderAll(); } }; // Update style for selected object const updateSelectedStyle = (key, value) => { const c = fabricRef.current; const obj = c.getActiveObject(); if (!obj) return; obj.set(key, value); if (key === "fill") setFillColor(value); if (key === "fontSize") setFontSize(value); c.requestRenderAll(); pushHistory(); }; // Simple export of JSON const exportJSON = () => { const c = fabricRef.current; const json = c.toDatalessJSON(["__uid"]); const blob = new Blob([JSON.stringify(json)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "design.json"; a.click(); }; return (

Canva‑style Editor (Demo)

{/* Toolbar */} {/* Canvas area */}
Tip: Double-click text to edit. Use handles to resize/rotate.
); } --- # ✅ Full MVP Repo (Frontend + Minimal Backend) Below is a complete, copy‑paste ready scaffold. Create a folder named `canva-mvp`, then create files exactly as shown. ## 📁 Folder Tree ``` canva-mvp/ ├─ README.md ├─ docker-compose.yml ├─ server/ │ ├─ package.json │ ├─ server.js │ ├─ storage/ │ │ └─ .gitkeep │ └─ uploads/ │ └─ .gitkeep └─ web/ ├─ index.html ├─ package.json ├─ postcss.config.js ├─ tailwind.config.js ├─ vite.config.js └─ src/ ├─ main.jsx ├─ index.css ├─ App.jsx ├─ lib/fabricSetup.js ├─ store/history.js ├─ components/Toolbar.jsx ├─ components/BottomBar.jsx ├─ components/LayerPanel.jsx ├─ components/CanvasStage.jsx └─ components/ColorPicker.jsx ``` --- ## 🐳 `docker-compose.yml` ```yaml version: "3.9" services: server: build: ./server ports: - "5050:5050" volumes: - ./server/storage:/app/storage - ./server/uploads:/app/uploads environment: - PORT=5050 web: working_dir: /app image: node:20-alpine command: sh -c "npm install && npm run dev -- --host 0.0.0.0" volumes: - ./web:/app ports: - "5173:5173" depends_on: - server ``` --- ## 🧰 Server (Express minimal API) ### `server/package.json` ```json { "name": "canva-mvp-server", "version": "1.0.0", "type": "module", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "cors": "^2.8.5", "express": "^4.19.2", "multer": "^1.4.5-lts.1", "nanoid": "^5.0.7" } } ``` ### `server/Dockerfile` ```dockerfile FROM node:20-alpine WORKDIR /app COPY package.json . RUN npm install --production COPY server.js . RUN mkdir -p storage uploads EXPOSE 5050 CMD ["npm","start"] ``` ### `server/server.js` ```js import express from 'express'; import cors from 'cors'; import fs from 'fs'; import path from 'path'; import multer from 'multer'; import { nanoid } from 'nanoid'; const app = express(); const PORT = process.env.PORT || 5050; const __dirname = path.resolve(); const STORAGE = path.join(__dirname, 'storage'); const UPLOADS = path.join(__dirname, 'uploads'); if (!fs.existsSync(STORAGE)) fs.mkdirSync(STORAGE, { recursive: true }); if (!fs.existsSync(UPLOADS)) fs.mkdirSync(UPLOADS, { recursive: true }); app.use(cors()); app.use(express.json({ limit: '20mb' })); app.use('/uploads', express.static(UPLOADS)); // Save/Load design JSON app.post('/api/projects', (req, res) => { const id = nanoid(); fs.writeFileSync(path.join(STORAGE, `${id}.json`), JSON.stringify(req.body||{}), 'utf8'); res.json({ id }); }); app.put('/api/projects/:id', (req, res) => { const file = path.join(STORAGE, `${req.params.id}.json`); if (!fs.existsSync(file)) return res.status(404).json({ error: 'Not found' }); fs.writeFileSync(file, JSON.stringify(req.body||{}), 'utf8'); res.json({ ok: true }); }); app.get('/api/projects/:id', (req, res) => { const file = path.join(STORAGE, `${req.params.id}.json`); if (!fs.existsSync(file)) return res.status(404).json({ error: 'Not found' }); res.json(JSON.parse(fs.readFileSync(file, 'utf8'))); }); app.get('/api/projects', (req, res) => { const list = fs.readdirSync(STORAGE).filter(f=>f.endsWith('.json')).map(f=>({ id:f.replace('.json','') })); res.json(list); }); // Upload assets const upload = multer({ dest: UPLOADS }); app.post('/api/upload', upload.single('file'), (req, res) => { res.json({ url: `/uploads/${req.file.filename}` }); }); app.listen(PORT, () => console.log(`API on http://localhost:${PORT}`)); ``` --- ## 🌐 Web (Vite + React + Tailwind + Fabric) ### `web/package.json` ```json { "name": "canva-mvp-web", "version": "1.0.0", "private": true, "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "fabric": "^6.0.0-beta12", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "autoprefixer": "^10.4.19", "postcss": "^8.4.41", "tailwindcss": "^3.4.10", "vite": "^5.4.0" } } ``` ### `web/vite.config.js` ```js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()] }) ``` ### `web/postcss.config.js` ```js export default { plugins: { tailwindcss: {}, autoprefixer: {} } } ``` ### `web/tailwind.config.js` ```js /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html","./src/**/*.{js,jsx}"], theme: { extend: {} }, plugins: [], } ``` ### `web/index.html` ```html Imon Designer
``` ### `web/src/index.css` ```css @tailwind base; @tailwind components; @tailwind utilities; :root{ --header:linear-gradient(90deg,#6a5cf6,#7f53ff,#a164ff); } ``` ### `web/src/main.jsx` ```jsx import React from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' createRoot(document.getElementById('root')).render() ``` ### `web/src/lib/fabricSetup.js` ```js import { fabric } from 'fabric' export function makeCanvas(node, opts={}){ const c = new fabric.Canvas(node, { backgroundColor:'#ffffff', width:1200, height:700, preserveObjectStacking:true, ...opts }) return c } ``` ### `web/src/store/history.js` ```js export function makeHistory(){ let stack=[], i=-1 return { push(json){ stack = stack.slice(0,i+1); stack.push(json); i=stack.length-1 }, undo(){ if(i<=0) return null; i--; return stack[i] }, redo(){ if(i>=stack.length-1) return null; i++; return stack[i] }, index(){ return i } } } ``` ### `web/src/components/ColorPicker.jsx` ```jsx export default function ColorPicker({value,onChange}){ return ( onChange(e.target.value)} /> ) } ``` ### `web/src/components/Toolbar.jsx` ```jsx export default function Toolbar({onAdd, onUpload, fill, setFill, fontSize, setFontSize, onDelete, onExport}){ return ( ) } ``` ### `web/src/components/BottomBar.jsx` ```jsx export default function BottomBar({page,onAddPage}){ return (
Page {page}
DesignElementsTextUploadsProjects
) } ``` ### `web/src/components/LayerPanel.jsx` ```jsx export default function LayerPanel({layers,onSelect,onUp,onDown}){ return (
{layers.length===0 &&
No layers
} {layers.map(l=> (
))}
) } ``` ### `web/src/components/CanvasStage.jsx` ```jsx import { useEffect, useRef } from 'react' import { fabric } from 'fabric' import { makeCanvas } from '../lib/fabricSetup' export default function CanvasStage({onReady}){ const ref = useRef() useEffect(()=>{ const c = makeCanvas(ref.current) onReady?.(c) return ()=> c.dispose() },[]) return (
) } ``` ### `web/src/App.jsx` ```jsx import React, { useEffect, useRef, useState } from 'react' import { fabric } from 'fabric' import Toolbar from './components/Toolbar' import BottomBar from './components/BottomBar' import LayerPanel from './components/LayerPanel' export default function App(){ const canvasRef = useRef(null) const [layers,setLayers] = useState([]) const [fill,setFill] = useState('#000000') const [fontSize,setFontSize] = useState(40) const [page,setPage] = useState(1) useEffect(()=>{ const c = new fabric.Canvas('editor',{backgroundColor:'#ffffff', width:1200, height:700, preserveObjectStacking:true}) canvasRef.current = c const refresh = ()=>{ const objs = c.getObjects().map((o)=>({ id:o.__uid||(o.__uid=crypto.randomUUID()), name:o.type, obj:o })) setLayers(objs.slice().reverse()) } c.on('object:added', refresh) c.on('object:removed', refresh) c.on('object:modified', refresh) refresh() return ()=> c.dispose() },[]) const add = (type)=>{ const c = canvasRef.current if(!c) return let obj if(type==='text') obj = new fabric.IText('Edit me',{ left:100, top:100, fill:fill, fontSize }) if(type==='rect') obj = new fabric.Rect({ left:150, top:150, width:150, height:100, fill:fill }) if(type==='circle') obj = new fabric.Circle({ left:200, top:200, radius:60, fill:fill }) if(type==='triangle') obj = new fabric.Triangle({ left:240, top:120, width:120, height:120, fill:fill }) if(!obj) return obj.__uid = crypto.randomUUID() c.add(obj).setActiveObject(obj); c.renderAll() } const upload = (e)=>{ const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = (f)=>{ fabric.Image.fromURL(f.target.result,(img)=>{ img.set({ left:120, top:120, scaleX:0.6, scaleY:0.6 }); img.__uid = crypto.randomUUID(); canvasRef.current.add(img).setActiveObject(img); canvasRef.current.renderAll(); }) } reader.readAsDataURL(file); e.target.value=null } const del = ()=>{ const c = canvasRef.current; const a = c.getActiveObject(); if(!a) return; if(a.type==='activeSelection'){ a.forEachObject(o=>c.remove(o)) } else c.remove(a); c.discardActiveObject(); c.renderAll() } const exportPNG = ()=>{ const c = canvasRef.current const url = c.toDataURL({ format:'png', quality:1 }) const a = document.createElement('a'); a.href=url; a.download='design.png'; a.click() } const selectLayer = (id)=>{ const c = canvasRef.current; const o = c.getObjects().find(o=>o.__uid===id); if(o){ c.setActiveObject(o); c.renderAll() } } const up = (id)=>{ const c = canvasRef.current; const o=c.getObjects().find(o=>o.__uid===id); if(o){ c.bringForward(o); c.renderAll() } } const down = (id)=>{ const c = canvasRef.current; const o=c.getObjects().find(o=>o.__uid===id); if(o){ c.sendBackwards(o); c.renderAll() } } return (
Imon Designer
Double‑tap text to edit. Use handles to resize/rotate.
setPage(p=>p+1)} />
) } ``` --- ## ▶️ Run (Local or Docker) ### Option 1: Docker (easiest) ```bash # from canva-mvp/ docker compose up --build # Web: http://localhost:5173 # API: http://localhost:5050 ``` ### Option 2: Manual ```bash # API cd server && npm install && npm start # Web cd ../web && npm install && npm run dev ``` --- ## Next Feature Upgrades (ask me to add any): - Multi‑page export (PDF), alignment tools, snapping & rulers - Background remover (self‑hosted) + filters - Brand kit (colors, fonts), template gallery & search - Real‑time collaboration (Yjs), comments, version history - Auth + projects backend with DB (Postgres/SQLite) and uploads to MinIO --- **Done!** Copy files as above, run, এবং তুমি ইতিমধ্যে Canva‑style MVP এডিটর পেয়ে গেলে। চাইলে আমি এখনই: (1) PDF/SVG এক্সপোর্ট, (2) snap‑to‑grid + align tools, (3) mobile toolbar exactly like your screenshots — এগুলো অ্যাড করে দিতে পারি। --- # 🚀 UPGRADE: All requested features added (code below) > এই সেকশনে আগের MVP-র ওপর **সব ফিচার** যোগ করা হয়েছে: PNG/SVG/PDF এক্সপোর্ট, snap-to-grid, align tools, rulers, mobile bottom bar, background remove (self-hosted), auth+projects (SQLite), Docker services (Rembg, Yjs)। ## 📁 Updated Folder Tree ``` canva-mvp/ ├─ README.md ├─ docker-compose.yml ← updated (rembg + yjs) ├─ server/ │ ├─ package.json ← updated (auth + sqlite) │ ├─ Dockerfile │ ├─ server.js ← updated APIs │ ├─ db.js ← NEW (SQLite) │ ├─ storage/ │ └─ uploads/ └─ web/ ├─ package.json ← updated (jspdf, yjs) └─ src/ ├─ lib/fabricSetup.js ← updated (snap + align) ├─ App.jsx ← updated (exports + BG remove + UI) ├─ ... (others remain) ``` --- ## 🐳 `docker-compose.yml` (REPLACE with this) ```yaml version: "3.9" services: server: build: ./server ports: - "5050:5050" volumes: - ./server/storage:/app/storage - ./server/uploads:/app/uploads environment: - PORT=5050 - JWT_SECRET=supersecret_change_me web: working_dir: /app image: node:20-alpine command: sh -c "npm install && npm run dev -- --host 0.0.0.0" volumes: - ./web:/app ports: - "5173:5173" depends_on: - server - yjs # 🧠 Background remover (self-hosted, offline) rembg: image: danielgatis/rembg:latest ports: - "7000:7000" command: s --host 0.0.0.0 --port 7000 # 🔗 Yjs websocket server for real-time collaboration yjs: image: node:20-alpine working_dir: /srv command: sh -c "npm init -y && npm i y-websocket && node -e \"require('y-websocket/bin/server.js')\"" ports: - "1234:1234" ``` --- ## 🧰 Server updates ### `server/package.json` (REPLACE) ```json { "name": "canva-mvp-server", "version": "2.0.0", "type": "module", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "bcryptjs": "^2.4.3", "cors": "^2.8.5", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "nanoid": "^5.0.7", "better-sqlite3": "^9.4.0" } } ``` ### `server/db.js` (NEW) ```js import Database from 'better-sqlite3' import path from 'path' const file = path.join(process.cwd(),'storage','app.sqlite') export const db = new Database(file) export function initDb(){ db.exec(`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, email TEXT UNIQUE, pass TEXT); CREATE TABLE IF NOT EXISTS projects (id TEXT PRIMARY KEY, owner TEXT, created DEFAULT CURRENT_TIMESTAMP);`) } ``` ### `server/server.js` (REPLACE) ```js import express from 'express' import cors from 'cors' import fs from 'fs' import path from 'path' import multer from 'multer' import { nanoid } from 'nanoid' import jwt from 'jsonwebtoken' import bcrypt from 'bcryptjs' import { db, initDb } from './db.js' const app = express() const PORT = process.env.PORT || 5050 const __dirname = path.resolve() const STORAGE = path.join(__dirname, 'storage') const UPLOADS = path.join(__dirname, 'uploads') if (!fs.existsSync(STORAGE)) fs.mkdirSync(STORAGE, { recursive: true }) if (!fs.existsSync(UPLOADS)) fs.mkdirSync(UPLOADS, { recursive: true }) initDb() app.use(cors()) app.use(express.json({ limit: '20mb' })) app.use('/uploads', express.static(UPLOADS)) const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret' function auth(req,res,next){ const hdr = req.headers.authorization||'' const token = hdr.startsWith('Bearer ')? hdr.slice(7): null if(!token) return res.status(401).json({error:'unauthorized'}) try{ req.user = jwt.verify(token, JWT_SECRET); next() }catch(e){ return res.status(401).json({error:'invalid token'}) } } // Auth app.post('/api/auth/register', (req,res)=>{ const { email, password } = req.body if(!email||!password) return res.status(400).json({error:'email & password required'}) const hash = bcrypt.hashSync(password, 10) try{ db.prepare('INSERT INTO users (id,email,pass) VALUES (?,?,?)').run(nanoid(), email, hash) } catch(e){ return res.status(400).json({error:'email exists'}) } return res.json({ ok:true }) }) app.post('/api/auth/login', (req,res)=>{ const { email, password } = req.body const row = db.prepare('SELECT * FROM users WHERE email=?').get(email) if(!row) return res.status(401).json({error:'invalid'}) if(!bcrypt.compareSync(password, row.pass)) return res.status(401).json({error:'invalid'}) const token = jwt.sign({ uid: row.id, email }, JWT_SECRET, { expiresIn:'7d' }) res.json({ token }) }) // Projects app.post('/api/projects', auth, (req, res) => { const id = nanoid() const data = JSON.stringify(req.body||{}) fs.writeFileSync(path.join(STORAGE, `${id}.json`), data, 'utf8') db.prepare('INSERT INTO projects (id, owner) VALUES (?,?)').run(id, req.user.uid) res.json({ id }) }) app.put('/api/projects/:id', auth, (req, res) => { const pid = req.params.id const row = db.prepare('SELECT * FROM projects WHERE id=? AND owner=?').get(pid, req.user.uid) if(!row) return res.status(404).json({ error:'Not found' }) fs.writeFileSync(path.join(STORAGE, `${pid}.json`), JSON.stringify(req.body||{}), 'utf8') res.json({ ok:true }) }) app.get('/api/projects', auth, (req,res)=>{ const list = db.prepare('SELECT id FROM projects WHERE owner=? ORDER BY created DESC').all(req.user.uid) res.json(list) }) app.get('/api/projects/:id', auth, (req,res)=>{ const pid = req.params.id const row = db.prepare('SELECT * FROM projects WHERE id=? AND owner=?').get(pid, req.user.uid) if(!row) return res.status(404).json({error:'Not found'}) res.json(JSON.parse(fs.readFileSync(path.join(STORAGE, `${pid}.json`),'utf8'))) }) // Upload assets const upload = multer({ dest: UPLOADS }) app.post('/api/upload', auth, upload.single('file'), (req,res)=>{ res.json({ url: `/uploads/${req.file.filename}` }) }) app.listen(PORT, ()=> console.log(`API on http://localhost:${PORT}`)) ``` --- ## 🌐 Web updates ### `web/package.json` (REPLACE) ```json { "name": "canva-mvp-web", "version": "2.0.0", "private": true, "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "fabric": "^6.0.0-beta12", "jspdf": "^2.5.1", "yjs": "^13.6.15", "y-websocket": "^1.5.10", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "autoprefixer": "^10.4.19", "postcss": "^8.4.41", "tailwindcss": "^3.4.10", "vite": "^5.4.0" } } ``` ### `web/src/lib/fabricSetup.js` (REPLACE) ```js import { fabric } from 'fabric' export function makeCanvas(node, opts={}){ const c = new fabric.Canvas(node, { backgroundColor:'#ffffff', width:1200, height:700, preserveObjectStacking:true, ...opts }) // Snap-to-grid (10px) const GRID = 10 c.on('object:moving', (e)=>{ const o=e.target; if(!o) return; o.set({ left: Math.round(o.left/GRID)*GRID, top: Math.round(o.top/GRID)*GRID }) }) c.on('object:scaling', (e)=>{ const o=e.target; if(!o) return; o.set({ scaleX: Math.round((o.scaleX||1)*10)/10, scaleY: Math.round((o.scaleY||1)*10)/10 }) }) c.on('object:rotating', (e)=>{ const o=e.target; if(!o) return; o.set({ angle: Math.round((o.angle||0)/5)*5 }) }) c.requestRenderAll() return c } export function align(c, mode){ const a = c.getActiveObject(); if(!a) return const bounds = a.getBoundingRect(true) const W=c.getWidth(), H=c.getHeight() const map = { left: ()=> a.set({ left:0 }), centerX: ()=> a.set({ left:(W-bounds.width)/2 }), right: ()=> a.set({ left: W - bounds.width }), top: ()=> a.set({ top:0 }), centerY: ()=> a.set({ top:(H-bounds.height)/2 }), bottom: ()=> a.set({ top: H - bounds.height }) } map[mode]?.(); c.requestRenderAll() } ``` ### `web/src/App.jsx` (REPLACE) ```jsx import React, { useEffect, useRef, useState } from 'react' import { fabric } from 'fabric' import Toolbar from './components/Toolbar' import BottomBar from './components/BottomBar' import LayerPanel from './components/LayerPanel' import { align } from './lib/fabricSetup' import jsPDF from 'jspdf' export default function App(){ const canvasRef = useRef(null) const [layers,setLayers] = useState([]) const [fill,setFill] = useState('#000000') const [fontSize,setFontSize] = useState(40) const [pages,setPages] = useState([0]) const [page,setPage] = useState(1) useEffect(()=>{ const c = new fabric.Canvas('editor',{backgroundColor:'#ffffff', width:1200, height:700, preserveObjectStacking:true}) canvasRef.current = c const refresh = ()=>{ const objs = c.getObjects().map((o)=>({ id:o.__uid||(o.__uid=crypto.randomUUID()), name:o.type, obj:o })); setLayers(objs.slice().reverse()) } c.on('object:added', refresh); c.on('object:removed', refresh); c.on('object:modified', refresh) // rulers (simple ticks) const ctx = c.getContext(); const drawRulers = ()=>{ const W=c.getWidth(), H=c.getHeight(); ctx.save(); ctx.fillStyle='#f3f4f6'; ctx.fillRect(0,0,W,20); ctx.fillRect(0,0,20,H); ctx.fillStyle='#9ca3af'; for(let x=40;x c.dispose() },[]) const add = (type)=>{ const c = canvasRef.current; if(!c) return; let obj if(type==='text') obj = new fabric.IText('Edit me',{ left:100, top:100, fill:fill, fontSize }) if(type==='rect') obj = new fabric.Rect({ left:150, top:150, width:150, height:100, fill:fill }) if(type==='circle') obj = new fabric.Circle({ left:200, top:200, radius:60, fill:fill }) if(type==='triangle') obj = new fabric.Triangle({ left:240, top:120, width:120, height:120, fill:fill }) if(!obj) return; obj.__uid = crypto.randomUUID(); c.add(obj).setActiveObject(obj); c.renderAll() } const upload = (e)=>{ const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = (f)=>{ fabric.Image.fromURL(f.target.result,(img)=>{ img.set({ left:120, top:120, scaleX:0.6, scaleY:0.6 }); img.__uid = crypto.randomUUID(); canvasRef.current.add(img).setActiveObject(img); canvasRef.current.renderAll(); }) }; reader.readAsDataURL(file); e.target.value=null } const del = ()=>{ const c = canvasRef.current; const a = c.getActiveObject(); if(!a) return; if(a.type==='activeSelection'){ a.forEachObject(o=>c.remove(o)) } else c.remove(a); c.discardActiveObject(); c.renderAll() } const exportPNG = ()=>{ const c=canvasRef.current; const url=c.toDataURL({format:'png', quality:1}); const a=document.createElement('a'); a.href=url; a.download='design.png'; a.click() } const exportSVG = ()=>{ const c=canvasRef.current; const svg=c.toSVG(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='design.svg'; a.click() } const exportPDF = ()=>{ const c=canvasRef.current; const pdf = new jsPDF({ unit:'px', format:[c.getWidth(), c.getHeight()] }); const img = c.toDataURL({ format:'png', quality:1 }); pdf.addImage(img, 'PNG', 0, 0, c.getWidth(), c.getHeight()); pdf.save('design.pdf') } const selectLayer = (id)=>{ const c = canvasRef.current; const o = c.getObjects().find(o=>o.__uid===id); if(o){ c.setActiveObject(o); c.renderAll() } } const up = (id)=>{ const c = canvasRef.current; const o=c.getObjects().find(o=>o.__uid===id); if(o){ c.bringForward(o); c.renderAll() } } const down = (id)=>{ const c = canvasRef.current; const o=c.getObjects().find(o=>o.__uid===id); if(o){ c.sendBackwards(o); c.renderAll() } } const removeBg = async ()=>{ const c = canvasRef.current; const a = c.getActiveObject(); if(!a || a.type!=='image') return alert('Select an image'); const data = a.getSrc(); const b = await fetch(data).then(r=>r.blob()); const form = new FormData(); form.append('file', b, 'image.png'); const out = await fetch('http://localhost:7000/api/remove', { method:'POST', body: form }).then(r=>r.blob()); const url = URL.createObjectURL(out); fabric.Image.fromURL(url,(img)=>{ img.set({ left:a.left, top:a.top, scaleX:a.scaleX, scaleY:a.scaleY }); img.__uid = a.__uid; c.remove(a); c.add(img).setActiveObject(img); c.renderAll() }) } return (
Imon Designer
Double‑tap text to edit. Snap to grid, rulers on. Select an image → Remove BG.
{ setPages(p=>[...p, p.length]); setPage(p=>p+1) }} />
) } ``` --- ## ▶️ Run (updated) ```bash # from canva-mvp/ docker compose up --build # Web: http://localhost:5173 # API: http://localhost:5050 # Rembg: http://localhost:7000 # Yjs: ws://localhost:1234 ``` ## Done & Next - Real‑time cursors, comments, template marketplace, brand kit UI—বললে আমি আরো যোগ করে দেব।