Files
ladill-events/resources/views/qr-codes/create.blade.php
T
isaaccladandCursor 5e00f319a5
Deploy Ladill Events / deploy (push) Successful in 35s
Split attendees and badge printing; add a 3-step event create flow.
Give each area its own sidebar entry and hub pages, and walk new events through details, QR design, then review before publish.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 12:27:42 +00:00

304 lines
20 KiB
PHP

<x-user-layout>
@php
$createType = old('type', $requestedType ?? \App\Models\QrCode::TYPE_EVENT);
$isProgramme = $createType === \App\Models\QrCode::TYPE_ITINERARY;
$defaultStyle = \App\Support\Qr\QrStyleDefaults::merge(old('style') ?: ($accountDefaultStyle ?? null));
@endphp
<x-slot name="title">{{ $isProgramme ? 'New programme' : 'Create event' }}</x-slot>
<div class="mx-auto max-w-6xl space-y-6"
x-data="qrCustomizer({
previewUrl: @js(route('events.style-preview')),
csrf: @js(csrf_token()),
shortCode: 'preview',
style: @js($defaultStyle),
balance: @js((float) $wallet->spendableBalance()),
price: @js((float) $pricePerQr),
topupUrl: @js($topupUrl),
type: @js($createType),
wizard: @js(! $isProgramme),
})">
@if(session('error'))
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{{ session('error') }}</div>
@endif
{{-- Mobile page header --}}
<div class="flex items-center gap-3 py-1 lg:hidden">
<a href="{{ $isProgramme ? route('programmes.index') : route('events.index') }}"
class="flex h-9 w-9 items-center justify-center rounded-xl border border-slate-200 bg-white shadow-sm text-slate-500 hover:text-slate-900 transition">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</a>
<span class="text-sm font-semibold text-slate-900">{{ $isProgramme ? 'New programme' : 'Create event' }}</span>
</div>
{{-- Desktop page header --}}
<div class="hidden lg:block">
<a href="{{ $isProgramme ? route('programmes.index') : route('events.index') }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-700">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
{{ $isProgramme ? 'Programmes' : 'Events' }}
</a>
<h1 class="mt-2 text-2xl font-bold text-slate-900">{{ $isProgramme ? 'New programme' : 'Create event' }}</h1>
<p class="mt-1 text-sm text-slate-500">
{{ $isProgramme ? 'Build a schedule outline and share it with attendees.' : 'Set up your event, then design the QR attendees will scan.' }}
</p>
</div>
@unless($isProgramme)
{{-- Event creation wizard steps --}}
<div class="rounded-2xl border border-slate-200 bg-white px-4 py-4 sm:px-6">
<ol class="flex items-center justify-between gap-2">
@foreach(['Event details', 'QR code', 'Review & create'] as $i => $label)
@php $n = $i + 1; @endphp
<li class="flex min-w-0 flex-1 items-center gap-2">
<span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-bold transition"
:class="step >= {{ $n }} ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-400'">{{ $n }}</span>
<span class="hidden truncate text-xs font-semibold sm:block"
:class="step >= {{ $n }} ? 'text-slate-900' : 'text-slate-400'">{{ $label }}</span>
@if($n < 3)
<span class="mx-1 hidden h-px flex-1 bg-slate-200 sm:block"></span>
@endif
</li>
@endforeach
</ol>
</div>
@endunless
<form x-ref="createForm" action="{{ route('events.store') }}" method="POST" enctype="multipart/form-data"
class="grid grid-cols-1 gap-8"
:class="(!wizard || step >= 2) ? 'lg:grid-cols-[380px_1fr]' : ''"
@submit="wizard ? wizardSubmit($event) : submitOrTopup($event)">
@csrf
<input type="hidden" name="type" value="{{ $createType }}">
{{-- QR preview (step 2 for events, always for programmes) --}}
<div class="lg:sticky lg:top-6 lg:self-start"
x-show="!wizard || step === 2 || step === 3"
x-cloak
:class="(!wizard || step >= 2) ? 'block' : 'hidden'">
<div class="hidden lg:block">
@include('qr-codes.partials.qr-preview-card', ['showDownloads' => false])
</div>
</div>
<div class="space-y-5 pb-4 lg:pb-0"
:class="wizard && step >= 2 ? 'lg:col-start-2' : (wizard && step === 1 ? 'lg:col-span-full' : '')">
{{-- Step 1 / Programme: Event details --}}
<div x-show="!wizard || step === 1" class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
<div class="border-b border-slate-100 bg-slate-50/70 px-5 py-4">
<h2 class="text-sm font-semibold text-slate-900">{{ $isProgramme ? 'Programme details' : 'Event details' }}</h2>
<p class="mt-0.5 text-xs text-slate-400">{{ $isProgramme ? 'Title, schedule, and venue for this outline.' : 'Name, dates, ticketing, and registration settings.' }}</p>
</div>
<div class="space-y-4 p-5">
<div>
<label class="block text-xs font-semibold text-slate-600">Label</label>
<input type="text" name="label" value="{{ old('label') }}" required maxlength="120"
class="mt-1.5 w-full rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-sm placeholder:text-slate-400 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30"
placeholder="{{ $isProgramme ? 'e.g. Annual conference programme' : 'e.g. Tech summit 2026' }}">
</div>
<div x-data="{
slug: @js(old('custom_short_code', '')),
status: '',
timer: null,
checkUrl: @js(route('events.check-slug')),
baseUrl: @js(\App\Models\QrCode::publicBaseUrl() . '/q'),
check() {
clearTimeout(this.timer);
const s = this.slug.trim();
if (s === '') { this.status = ''; return; }
if (!/^[a-z0-9][a-z0-9-]{1,18}[a-z0-9]$/.test(s)) { this.status = 'invalid'; return; }
this.status = 'checking';
this.timer = setTimeout(() => {
fetch(this.checkUrl + '?code=' + encodeURIComponent(s))
.then(r => r.json())
.then(d => { this.status = d.available ? 'available' : 'taken'; })
.catch(() => { this.status = ''; });
}, 400);
},
}">
<label class="block text-xs font-semibold text-slate-600">Custom link <span class="font-normal text-slate-400">(optional)</span></label>
<div class="mt-1.5 flex items-stretch rounded-xl border focus-within:border-indigo-400 focus-within:ring-1 focus-within:ring-indigo-400/30"
:class="status === 'available' ? 'border-green-400' : status === 'taken' || status === 'invalid' ? 'border-red-400' : 'border-slate-200'">
<span class="flex items-center rounded-l-xl border-r border-slate-200 bg-slate-50 px-3 text-xs text-slate-400 whitespace-nowrap select-none"
:class="status === 'available' ? 'border-green-400' : status === 'taken' || status === 'invalid' ? 'border-red-400' : 'border-slate-200'">
{{ \App\Models\QrCode::publicBaseUrl() }}/q/
</span>
<input type="text" name="custom_short_code"
x-model="slug"
@input="check()"
maxlength="20"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
class="min-w-0 flex-1 rounded-r-xl bg-white px-3 py-2.5 text-sm placeholder:text-slate-400 focus:outline-none"
placeholder="my-brand">
</div>
<div class="mt-1.5 flex items-center gap-1.5 text-[11px]">
<template x-if="status === 'checking'"><span class="text-slate-400">Checking…</span></template>
<template x-if="status === 'available'">
<span class="flex items-center gap-1 text-green-600">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
Available
</span>
</template>
<template x-if="status === 'taken'"><span class="text-red-500">Already taken</span></template>
<template x-if="status === 'invalid'"><span class="text-red-500">Invalid format</span></template>
<template x-if="status === ''"><span class="text-slate-400">Leave blank to auto-generate</span></template>
</div>
</div>
<div class="space-y-3">
@include('qr-codes.partials.type-fields-create')
</div>
@unless($isProgramme)
<p class="rounded-xl bg-slate-50 px-3.5 py-2.5 text-xs leading-5 text-slate-400">
You will design the QR code in the next step. Details can be updated anytime without reprinting.
</p>
@else
<p class="rounded-xl bg-slate-50 px-3.5 py-2.5 text-xs leading-5 text-slate-400">
Attendees scan your Ladill event link update details anytime without reprinting the QR.
</p>
@endunless
</div>
</div>
{{-- Step 2: QR customization (events) or inline (programmes) --}}
<div x-show="!wizard || step === 2">
@include('qr-codes.partials.customization-fields', [
'style' => $defaultStyle,
'moduleStyles' => $moduleStyles,
'cornerOuterStyles' => $cornerOuterStyles,
'cornerInnerStyles' => $cornerInnerStyles,
'frameStyles' => $frameStyles,
])
</div>
{{-- Step 3: Review & create (events only) --}}
<div x-show="wizard && step === 3" x-cloak class="space-y-5">
<div class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
<div class="border-b border-slate-100 bg-slate-50/70 px-5 py-4">
<h2 class="text-sm font-semibold text-slate-900">Review your event</h2>
<p class="mt-0.5 text-xs text-slate-400">Confirm details before publishing. GHS {{ number_format($pricePerQr, 2) }} will be charged from your QR balance.</p>
</div>
<div class="space-y-3 p-5 text-sm">
<div class="flex justify-between gap-4 border-b border-slate-100 pb-3">
<span class="text-slate-500">Event</span>
<span class="font-semibold text-slate-900 text-right" x-text="reviewEventName()"></span>
</div>
<div class="flex justify-between gap-4 border-b border-slate-100 pb-3">
<span class="text-slate-500">Admin label</span>
<span class="font-semibold text-slate-900 text-right" x-text="reviewLabel()"></span>
</div>
<div class="flex justify-between gap-4">
<span class="text-slate-500">QR balance after</span>
<span class="font-semibold text-slate-900">GHS <span x-text="Math.max(0, balance - price).toFixed(2)"></span></span>
</div>
</div>
</div>
<div class="lg:hidden">
@include('qr-codes.partials.qr-preview-card', ['showDownloads' => false])
</div>
</div>
{{-- Desktop navigation --}}
<div class="hidden gap-3 lg:flex">
<button type="button" x-show="wizard && step > 1" x-cloak @click="prevStep()"
class="rounded-xl border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">
Back
</button>
<button type="button" x-show="wizard && step < 3" x-cloak @click="nextStep()"
class="flex-1 rounded-xl bg-indigo-600 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
Continue
</button>
<button type="button" x-show="!wizard || step === 3" @click="wizardSubmit($event)"
class="flex-1 rounded-xl bg-indigo-600 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
{{ $isProgramme ? 'Create programme' : 'Create event' }}
</button>
</div>
</div>
</form>
{{-- Mobile sticky action bar --}}
<div x-show="!showPreviewModal"
class="mobile-action-bar lg:hidden">
<div class="mobile-action-bar__toolbar">
<button type="button" x-show="wizard && step > 1" x-cloak @click="prevStep()"
class="rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-semibold text-slate-600">
Back
</button>
<button type="button" x-show="wizard && (step === 2 || step === 3)" @click="showPreviewModal = true"
class="flex flex-1 items-center justify-center gap-2 rounded-xl border border-slate-200 bg-slate-50 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-100">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
</svg>
Preview
</button>
<button type="button" x-show="wizard && step < 3" x-cloak @click="nextStep()"
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-indigo-600 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
Continue
</button>
<button type="button" x-show="!wizard || step === 3" @click="wizardSubmit($event)"
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-indigo-600 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
Create
</button>
</div>
<div class="mobile-action-bar__inset-fill" aria-hidden="true"></div>
</div>
{{-- Preview modal --}}
<div x-show="showPreviewModal" x-cloak
x-transition:enter="transition-opacity duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@keydown.escape.window="showPreviewModal = false"
class="fixed inset-0 z-[60] flex flex-col bg-black/60 backdrop-blur-sm lg:hidden"
style="padding-bottom: env(safe-area-inset-bottom, 0px)">
<div class="flex items-center justify-between px-5 py-4">
<span class="text-sm font-semibold text-white">Preview</span>
<button type="button" @click="showPreviewModal = false"
class="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 text-white hover:bg-white/30 transition">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="flex flex-1 items-center justify-center p-8">
<div class="w-full max-w-xs overflow-hidden rounded-2xl bg-white shadow-2xl"
:style="frameStyle === 'thin'
? 'box-shadow: 0 0 0 2px ' + frameColor + ', 0 25px 50px -12px rgb(0 0 0 / 0.25)'
: 'box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25), 0 0 0 1px rgb(0 0 0 / 0.06)'">
<div :class="{
'p-[8px]': frameStyle === 'thin',
'p-[14px]': frameStyle === 'scan_me' || frameStyle === 'tap_to_scan',
'p-4': frameStyle === 'none',
}">
<div x-ref="qrPreviewModal"
class="aspect-square w-full [&>svg]:block [&>svg]:h-full [&>svg]:w-full"></div>
<div x-show="frameStyle === 'scan_me'" x-cloak
:style="'border-top: 1px solid ' + frameColor + '40; color: ' + frameColor"
class="pt-2.5 pb-0.5 text-center">
<span x-text="(frameText && frameText.trim() ? frameText : 'SCAN ME').toUpperCase()"
class="text-[11px] font-bold tracking-[0.18em]"></span>
</div>
<div x-show="frameStyle === 'tap_to_scan'" x-cloak class="mt-2.5 flex justify-center pb-0.5">
<span x-text="(frameText && frameText.trim() ? frameText : 'TAP TO SCAN').toUpperCase()"
:style="'background-color: ' + frameColor + '; color: ' + frameTextColor"
class="rounded-full px-5 py-1.5 text-[10px] font-bold tracking-widest"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</x-user-layout>