Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Template Designer | QR Wizardry</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/react-dropzone@14.2.3/dist/react-dropzone.js"></script> | |
| <script src="https://unpkg.com/react-resizable@3.0.5/dist/react-resizable.js"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: { | |
| 50: '#eff6ff', | |
| 100: '#dbeafe', | |
| 200: '#bfdbfe', | |
| 300: '#93c5fd', | |
| 400: '#60a5fa', | |
| 500: '#3b82f6', | |
| 600: '#2563eb', | |
| 700: '#1d4ed8', | |
| 800: '#1e40af', | |
| 900: '#1e3a8a', | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #0f172a; | |
| color: #e2e8f0; | |
| } | |
| .resize-handle { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| background: #3b82f6; | |
| right: -5px; | |
| bottom: -5px; | |
| cursor: se-resize; | |
| border-radius: 2px; | |
| } | |
| .dropzone { | |
| border: 2px dashed #334155; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #3b82f6; | |
| background-color: #1e3a8a20; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen"> | |
| <div class="relative"> | |
| <!-- Navigation --> | |
| <nav class="bg-gray-900/80 backdrop-blur-sm border-b border-gray-800"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex items-center justify-between h-16"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0 flex items-center"> | |
| <i data-feather="maximize-2" class="text-primary-500 w-6 h-6"></i> | |
| <span class="ml-2 font-bold">QR Wizardry</span> | |
| </div> | |
| </div> | |
| <div class="hidden md:block"> | |
| <div class="ml-10 flex items-baseline space-x-4"> | |
| <a href="/" class="text-gray-400 hover:text-white px-3 py-2 rounded-md text-sm font-medium"> | |
| Home | |
| </a> | |
| <a href="#" class="bg-gray-800 text-white px-3 py-2 rounded-md text-sm font-medium"> | |
| Templates | |
| </a> | |
| <a href="#" class="text-gray-400 hover:text-white px-3 py-2 rounded-md text-sm font-medium"> | |
| Documentation | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main content --> | |
| <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
| <div class="mb-8"> | |
| <h1 class="text-3xl font-bold mb-2">Template Designer</h1> | |
| <p class="text-gray-400">Create your perfect QR code template with our intuitive designer</p> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| <!-- Export Section --> | |
| <div class="bg-gray-900/50 border border-gray-800 rounded-xl p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i data-feather="download" class="w-5 h-5 mr-2 text-primary-500"></i> | |
| Export Options | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Export Format</label> | |
| <select class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2"> | |
| <option>PNG</option> | |
| <option>JPG</option> | |
| <option>PDF</option> | |
| <option>SVG</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">DPI</label> | |
| <select class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2"> | |
| <option>72 (Web)</option> | |
| <option selected>300 (Print)</option> | |
| <option>600 (High Quality Print)</option> | |
| </select> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Width (mm)</label> | |
| <input type="number" value="65" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Height (mm)</label> | |
| <input type="number" value="140" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2"> | |
| </div> | |
| </div> | |
| <button id="exportCanvas" class="w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors flex items-center justify-center"> | |
| <i data-feather="download" class="w-4 h-4 mr-2"></i> | |
| Export Design | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Canvas Designer --> | |
| <div class="space-y-6"> | |
| <div class="bg-gray-900/50 border border-gray-800 rounded-xl p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i data-feather="edit-2" class="w-5 h-5 mr-2 text-primary-500"></i> | |
| Canvas Designer | |
| </h2> | |
| <div class="flex flex-col space-y-4"> | |
| <div class="flex space-x-4"> | |
| <button id="addQR" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 rounded-lg flex items-center"> | |
| <i data-feather="plus" class="w-4 h-4 mr-2"></i> Add QR | |
| </button> | |
| <button id="addText" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg flex items-center"> | |
| <i data-feather="type" class="w-4 h-4 mr-2"></i> Add Text | |
| </button> | |
| <button id="addImage" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg flex items-center"> | |
| <i data-feather="image" class="w-4 h-4 mr-2"></i> Add Image | |
| </button> | |
| </div> | |
| <div class="flex space-x-4"> | |
| <button id="deleteSelected" class="px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg flex items-center"> | |
| <i data-feather="trash-2" class="w-4 h-4 mr-2"></i> Delete | |
| </button> | |
| <button id="clearCanvas" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg flex items-center"> | |
| <i data-feather="x" class="w-4 h-4 mr-2"></i> Clear All | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-6 relative"> | |
| <div class="border-2 border-dashed border-gray-700 rounded-lg overflow-hidden"> | |
| <canvas id="designCanvas" class="bg-white w-full" style="height: 600px;"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Properties Panel --> | |
| <div class="bg-gray-900/50 border border-gray-800 rounded-xl p-6" id="propertiesPanel"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i data-feather="sliders" class="w-5 h-5 mr-2 text-primary-500"></i> | |
| Properties | |
| </h2> | |
| <div class="space-y-4" id="propertyControls"> | |
| <div class="text-gray-400 italic text-center py-8"> | |
| Select an element to edit its properties | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Hidden elements for export --> | |
| <a id="downloadLink" style="display: none;"></a> | |
| <!-- Footer --> | |
| <footer class="border-t border-gray-800 py-8 mt-12"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex flex-col md:flex-row justify-between items-center"> | |
| <div class="flex items-center space-x-2 mb-4 md:mb-0"> | |
| <div class="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center"> | |
| <i data-feather="maximize-2" class="text-white w-4 h-4"></i> | |
| </div> | |
| <span class="text-lg font-bold">QR Wizardry</span> | |
| </div> | |
| <div class="text-sm text-gray-500"> | |
| © 2023 QR Wizardry Portal. All rights reserved. | |
| </div> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script> | |
| <script> | |
| feather.replace(); | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const canvas = new fabric.Canvas('designCanvas', { | |
| backgroundColor: '#ffffff', | |
| selection: true, | |
| preserveObjectStacking: true | |
| }); | |
| // Add QR code element | |
| document.getElementById('addQR').addEventListener('click', function() { | |
| fabric.Image.fromURL('https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=Example', function(img) { | |
| img.set({ | |
| left: 50, | |
| top: 50, | |
| scaleX: 1, | |
| scaleY: 1, | |
| cornerStyle: 'circle', | |
| cornerColor: '#3b82f6', | |
| cornerSize: 10, | |
| transparentCorners: false, | |
| borderColor: '#3b82f6', | |
| borderScaleFactor: 1.5, | |
| hasRotatingPoint: true | |
| }); | |
| canvas.add(img); | |
| canvas.setActiveObject(img); | |
| updatePropertiesPanel(img); | |
| }); | |
| }); | |
| // Add text element | |
| document.getElementById('addText').addEventListener('click', function() { | |
| const text = new fabric.Text('Double click to edit', { | |
| left: 100, | |
| top: 100, | |
| fontFamily: 'Inter', | |
| fontSize: 20, | |
| fill: '#000000', | |
| hasControls: true, | |
| cornerStyle: 'circle', | |
| cornerColor: '#3b82f6', | |
| cornerSize: 10, | |
| transparentCorners: false | |
| }); | |
| canvas.add(text); | |
| canvas.setActiveObject(text); | |
| updatePropertiesPanel(text); | |
| text.on('editing:entered', function() { | |
| text.set('fill', '#3b82f6'); | |
| canvas.renderAll(); | |
| }); | |
| text.on('editing:exited', function() { | |
| text.set('fill', '#000000'); | |
| canvas.renderAll(); | |
| }); | |
| }); | |
| // Add image element | |
| document.getElementById('addImage').addEventListener('click', function() { | |
| const input = document.createElement('input'); | |
| input.type = 'file'; | |
| input.accept = 'image/*'; | |
| input.onchange = function(e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| fabric.Image.fromURL(event.target.result, function(img) { | |
| img.set({ | |
| left: 150, | |
| top: 150, | |
| scaleX: 0.5, | |
| scaleY: 0.5, | |
| hasControls: true, | |
| cornerStyle: 'circle', | |
| cornerColor: '#3b82f6', | |
| cornerSize: 10, | |
| transparentCorners: false | |
| }); | |
| canvas.add(img); | |
| canvas.setActiveObject(img); | |
| updatePropertiesPanel(img); | |
| }); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }; | |
| input.click(); | |
| }); | |
| // Delete selected element | |
| document.getElementById('deleteSelected').addEventListener('click', function() { | |
| const activeObject = canvas.getActiveObject(); | |
| if (activeObject) { | |
| canvas.remove(activeObject); | |
| updatePropertiesPanel(null); | |
| } | |
| }); | |
| // Clear canvas | |
| document.getElementById('clearCanvas').addEventListener('click', function() { | |
| if (confirm('Are you sure you want to clear the canvas?')) { | |
| canvas.clear(); | |
| updatePropertiesPanel(null); | |
| } | |
| }); | |
| // Canvas selection event | |
| canvas.on('selection:created', function(options) { | |
| updatePropertiesPanel(options.selected[0]); | |
| }); | |
| canvas.on('selection:updated', function(options) { | |
| updatePropertiesPanel(options.selected[0]); | |
| }); | |
| canvas.on('selection:cleared', function() { | |
| updatePropertiesPanel(null); | |
| }); | |
| // Update properties panel based on selected object | |
| function updatePropertiesPanel(obj) { | |
| const panel = document.getElementById('propertyControls'); | |
| if (!obj) { | |
| panel.innerHTML = ` | |
| <div class="text-gray-400 italic text-center py-8"> | |
| Select an element to edit its properties | |
| </div> | |
| `; | |
| return; | |
| } | |
| let controls = ''; | |
| // Common properties | |
| controls += ` | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Position</label> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <div> | |
| <input type="number" class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1" | |
| id="posX" value="${Math.round(obj.left)}" placeholder="X"> | |
| </div> | |
| <div> | |
| <input type="number" class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1" | |
| id="posY" value="${Math.round(obj.top)}" placeholder="Y"> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Size</label> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <div> | |
| <input type="number" class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1" | |
| id="width" value="${Math.round(obj.width * obj.scaleX)}" placeholder="Width"> | |
| </div> | |
| <div> | |
| <input type="number" class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1" | |
| id="height" value="${Math.round(obj.height * obj.scaleY)}" placeholder="Height"> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Rotation</label> | |
| <input type="range" class="w-full bg-gray-800" id="rotation" | |
| min="0" max="360" value="${Math.round(obj.angle || 0)}"> | |
| </div> | |
| `; | |
| // Text specific properties | |
| if (obj.type === 'text') { | |
| controls += ` | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Text Content</label> | |
| <textarea class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1" | |
| id="textContent" rows="2">${obj.text}</textarea> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Font Size</label> | |
| <input type="number" class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1" | |
| id="fontSize" value="${obj.fontSize}"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Text Color</label> | |
| <input type="color" class="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1" | |
| id="textColor" value="${rgbToHex(obj.fill)}"> | |
| </div> | |
| `; | |
| } | |
| // Image specific properties | |
| if (obj.type === 'image') { | |
| controls += ` | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Opacity</label> | |
| <input type="range" class="w-full bg-gray-800" id="opacity" | |
| min="0" max="1" step="0.1" value="${obj.opacity}"> | |
| </div> | |
| `; | |
| } | |
| panel.innerHTML = controls; | |
| // Add event listeners for property changes | |
| document.getElementById('posX')?.addEventListener('change', function(e) { | |
| obj.set('left', parseInt(e.target.value)); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('posY')?.addEventListener('change', function(e) { | |
| obj.set('top', parseInt(e.target.value)); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('width')?.addEventListener('change', function(e) { | |
| const scale = parseInt(e.target.value) / obj.width; | |
| obj.set('scaleX', scale); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('height')?.addEventListener('change', function(e) { | |
| const scale = parseInt(e.target.value) / obj.height; | |
| obj.set('scaleY', scale); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('rotation')?.addEventListener('input', function(e) { | |
| obj.set('angle', parseInt(e.target.value)); | |
| canvas.renderAll(); | |
| }); | |
| if (obj.type === 'text') { | |
| document.getElementById('textContent')?.addEventListener('change', function(e) { | |
| obj.set('text', e.target.value); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('fontSize')?.addEventListener('change', function(e) { | |
| obj.set('fontSize', parseInt(e.target.value)); | |
| canvas.renderAll(); | |
| }); | |
| document.getElementById('textColor')?.addEventListener('change', function(e) { | |
| obj.set('fill', e.target.value); | |
| canvas.renderAll(); | |
| }); | |
| } | |
| if (obj.type === 'image') { | |
| document.getElementById('opacity')?.addEventListener('input', function(e) { | |
| obj.set('opacity', parseFloat(e.target.value)); | |
| canvas.renderAll(); | |
| }); | |
| } | |
| } | |
| // Helper function to convert RGB to HEX | |
| function rgbToHex(rgb) { | |
| if (!rgb) return '#000000'; | |
| if (typeof rgb === 'string' && rgb.startsWith('#')) return rgb; | |
| return '#' + | |
| (rgb[0] | 1 << 8).toString(16).slice(1) + | |
| (rgb[1] | 1 << 8).toString(16).slice(1) + | |
| (rgb[2] | 1 << 8).toString(16).slice(1); | |
| } | |
| // Set canvas size to fit container | |
| function resizeCanvas() { | |
| const container = document.querySelector('.border-2.border-dashed'); | |
| if (container) { | |
| const width = container.clientWidth; | |
| canvas.setWidth(width); | |
| canvas.setHeight(600); // Fixed height for demo | |
| canvas.renderAll(); | |
| } | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |