200 lines
12 KiB
PHP
200 lines
12 KiB
PHP
<div class="grid grid-cols-1 lg:grid-cols-2 gap-0 max-h-[85vh]">
|
||
|
||
{{-- LEFT: Form --}}
|
||
<div class="p-6 overflow-y-auto border-r border-white/[.06]">
|
||
<h2 class="text-lg font-semibold text-t1 mb-4">Create QR Code</h2>
|
||
|
||
<div class="mb-4">
|
||
<label class="block text-xs font-medium text-t2 mb-1.5">Label (optional)</label>
|
||
<input wire:model.live="label" type="text" placeholder="My QR Code"
|
||
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50">
|
||
</div>
|
||
|
||
<div class="flex gap-0.5 mb-4 border-b border-white/[.06] overflow-x-auto -mx-6 px-6">
|
||
@foreach(['url'=>'URL','vcard'=>'vCard','wifi'=>'WiFi','email'=>'Email','phone'=>'Phone','sms'=>'SMS','text'=>'Text'] as $key => $lbl)
|
||
<button wire:click="$set('type','{{ $key }}')" type="button"
|
||
class="px-3 py-2 text-xs whitespace-nowrap border-b-2 transition-colors -mb-px
|
||
{{ $type === $key ? 'border-blue text-blue font-medium' : 'border-transparent text-t2 hover:text-t1' }}">
|
||
{{ $lbl }}
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
|
||
<div class="space-y-3 mb-4">
|
||
@if(in_array($type, ['url','text','location']))
|
||
<input wire:model.live.debounce.250ms="url" type="{{ $type === 'url' ? 'url' : 'text' }}"
|
||
placeholder="{{ $type === 'url' ? 'https://example.com' : ($type === 'location' ? '48.8566,2.3522' : 'Your text') }}"
|
||
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50">
|
||
@elseif($type === 'email')
|
||
<input wire:model.live.debounce.250ms="url" type="email" placeholder="email@example.com"
|
||
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
@elseif($type === 'phone')
|
||
<input wire:model.live.debounce.250ms="url" type="tel" placeholder="+49 123 456789"
|
||
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
@elseif($type === 'sms')
|
||
<input wire:model.live.debounce.250ms="url" type="tel" placeholder="+49 123 456789"
|
||
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
@elseif($type === 'vcard')
|
||
<input wire:model.live="vcardName" placeholder="Full Name" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
<input wire:model.live="vcardPhone" placeholder="Phone" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
<input wire:model.live="vcardEmail" placeholder="Email" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
<input wire:model.live="vcardCompany" placeholder="Company" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
<input wire:model.live="vcardWebsite" placeholder="Website" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
@elseif($type === 'wifi')
|
||
<input wire:model.live="wifiSsid" placeholder="Network name (SSID)" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
<input wire:model.live="wifiPassword" type="password" placeholder="Password" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
<select wire:model.live="wifiEncryption" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none">
|
||
<option value="WPA">WPA/WPA2</option>
|
||
<option value="WEP">WEP</option>
|
||
<option value="nopass">Open</option>
|
||
</select>
|
||
<label class="flex items-center gap-2 text-sm text-t2 cursor-pointer">
|
||
<input wire:model.live="wifiHidden" type="checkbox"> Hidden network
|
||
</label>
|
||
@endif
|
||
</div>
|
||
|
||
<div x-data="{ open: false }" class="border border-white/[.06] rounded-lg mb-2">
|
||
<button type="button" @click="open = !open"
|
||
class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium text-t1 select-none">
|
||
<span>Styling</span>
|
||
<svg class="w-4 h-4 text-t3 transition-transform" :class="open && 'rotate-180'" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6l4 4 4-4"/>
|
||
</svg>
|
||
</button>
|
||
<div x-show="open" class="px-4 pb-4 space-y-3 border-t border-white/[.06] pt-3">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-xs text-t2 mb-1">Foreground</label>
|
||
<input wire:model.live="fgColor" type="color" class="w-full h-9 rounded cursor-pointer border border-white/[.06]">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-t2 mb-1">Background</label>
|
||
<input wire:model.live="bgColor" type="color" class="w-full h-9 rounded cursor-pointer border border-white/[.06]">
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-t2 mb-1">Error Correction</label>
|
||
<select wire:model.live="errorCorrection" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none">
|
||
<option value="L">L — 7%</option>
|
||
<option value="M">M — 15%</option>
|
||
<option value="Q">Q — 25%</option>
|
||
<option value="H">H — 30%</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-t2 mb-1">Size: <span class="text-t1">{{ $size }}px</span></label>
|
||
<input wire:model.live="size" type="range" min="128" max="1024" step="32" class="w-full accent-blue">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-t2 mb-1">Logo URL (optional)</label>
|
||
<input wire:model.live.debounce.500ms="logoUrl" type="url" placeholder="https://..."
|
||
class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-data="{ open: false }" class="border border-white/[.06] rounded-lg mb-2">
|
||
<button type="button" @click="open = !open"
|
||
class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium text-t1 select-none">
|
||
<span>Frame</span>
|
||
<svg class="w-4 h-4 text-t3 transition-transform" :class="open && 'rotate-180'" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6l4 4 4-4"/>
|
||
</svg>
|
||
</button>
|
||
<div x-show="open" class="px-4 pb-4 space-y-3 border-t border-white/[.06] pt-3">
|
||
<select wire:model.live="frame" class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none">
|
||
<option value="none">No frame</option>
|
||
<option value="label-bottom">Label bottom</option>
|
||
<option value="label-top">Label top</option>
|
||
</select>
|
||
<input wire:model.live="frameText" placeholder='e.g. "Scan me"'
|
||
class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
<div>
|
||
<label class="block text-xs text-t2 mb-1">Frame Color</label>
|
||
<input wire:model.live="frameColor" type="color" class="w-full h-9 rounded cursor-pointer border border-white/[.06]">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-data="{ open: false }" class="border border-white/[.06] rounded-lg mb-4">
|
||
<button type="button" @click="open = !open"
|
||
class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium text-t1 select-none">
|
||
<span>Advanced</span>
|
||
<svg class="w-4 h-4 text-t3 transition-transform" :class="open && 'rotate-180'" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6l4 4 4-4"/>
|
||
</svg>
|
||
</button>
|
||
<div x-show="open" class="px-4 pb-4 space-y-3 border-t border-white/[.06] pt-3">
|
||
<label class="flex items-center gap-2 text-sm text-t2 cursor-pointer">
|
||
<input wire:model.live="isDynamic" type="checkbox"> Dynamic QR (target changeable later)
|
||
</label>
|
||
<div>
|
||
<label class="block text-xs text-t2 mb-1">Scan limit (optional)</label>
|
||
<input wire:model.live="scanLimit" type="number" min="1" placeholder="Unlimited"
|
||
class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-t2 mb-1">Expires at (optional)</label>
|
||
<input wire:model.live="expiresAt" type="datetime-local"
|
||
class="block w-full px-3 py-2 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-3">
|
||
<x-ui.button wire:click="save" action="save" label="Create QR Code" class="flex-1" />
|
||
<x-ui.button wire:click="closeModal" variant="secondary" label="Cancel" />
|
||
</div>
|
||
</div>
|
||
|
||
{{-- RIGHT: Live Preview --}}
|
||
<div class="bg-s2 p-6 flex flex-col items-center justify-center">
|
||
<div class="text-xs font-medium text-t3 uppercase tracking-wider mb-4">Live Preview</div>
|
||
<div id="qr-preview-wrap" wire:ignore class="rounded-xl p-4 shadow-inner flex items-center justify-center"
|
||
style="background: {{ $bgColor }}; min-width: 220px; min-height: 220px;">
|
||
<canvas id="qr-canvas" style="display:none;"></canvas>
|
||
<span id="qr-placeholder" class="text-xs text-gray-400">Enter content above</span>
|
||
</div>
|
||
<div class="text-xs text-t3 mt-3">{{ $size }}×{{ $size }}px · {{ strtoupper($errorCorrection) }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
@script
|
||
<script>
|
||
const QRCode = window.QRCode;
|
||
|
||
const canvas = document.getElementById('qr-canvas');
|
||
const placeholder = document.getElementById('qr-placeholder');
|
||
const wrap = document.getElementById('qr-preview-wrap');
|
||
|
||
async function renderQR(payload, options) {
|
||
const empty = !payload || ['mailto:', 'tel:', 'sms:', 'geo:', ''].includes(payload.trim());
|
||
if (empty) {
|
||
canvas.style.display = 'none';
|
||
placeholder.style.display = 'block';
|
||
return;
|
||
}
|
||
try {
|
||
placeholder.style.display = 'none';
|
||
canvas.style.display = 'block';
|
||
wrap.style.background = options.bgColor || '#ffffff';
|
||
await QRCode.toCanvas(canvas, payload, {
|
||
width: Math.min(220, options.size || 220),
|
||
margin: options.margin || 4,
|
||
errorCorrectionLevel: options.errorCorrection || 'M',
|
||
color: { dark: options.fgColor || '#000000', light: options.bgColor || '#ffffff' },
|
||
});
|
||
} catch(e) { console.warn('QR:', e); }
|
||
}
|
||
|
||
$wire.on('qr-preview-update', ({ payload, options }) => renderQR(payload, options));
|
||
|
||
renderQR(@json($this->buildPayload()), {
|
||
size: @json($size), margin: @json($margin),
|
||
errorCorrection: @json($errorCorrection),
|
||
fgColor: @json($fgColor), bgColor: @json($bgColor),
|
||
});
|
||
</script>
|
||
@endscript
|