Files
ladill-merchant/resources/views/public/qr/booking-landing.blade.php
T
isaaccladandClaude Opus 4.8 00b1644887
Deploy Ladill Merchant / deploy (push) Successful in 35s
Storefront polish + QR share + importer deactivate fix
- Storefronts use Figtree as the default font (shop/menu, booking, book)
- Remove "Powered by Ladill" branding from the storefront header
- Fix storefront logo hiding behind the cover (logo row now relative z-10)
- Add a Share button to storefront QR downloads using the Web Share API
  (native mobile share sheet for image+link; clipboard fallback)
- storefronts:import-legacy: --deactivate-source now also retires the
  platform copy of already-imported (skipped) codes, no --refresh needed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:16:48 +00:00

157 lines
7.9 KiB
PHP

@php
$c = $qrCode->content();
$title = $c['booking_title'] ?? $qrCode->label ?? 'Book an appointment';
$services = $c['services'] ?? [];
$hasCover = ! empty($c['cover_path']);
$hasLogo = ! empty($c['logo_path']);
$brand = $c['brand_color'] ?? '#4f46e5';
$slotsUrl = route('qr.public.booking.slots', $qrCode->short_code);
$bookUrl = route('qr.public.booking.store', $qrCode->short_code);
$csrfToken = csrf_token();
@endphp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title }}</title>
@include('partials.favicon')
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>[x-cloak] { display: none !important; } body { font-family: 'Figtree', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }</style>
</head>
<body class="min-h-screen bg-slate-50 antialiased">
<div x-data="{
step: 1,
serviceIndex: 0,
date: '',
slots: [],
selectedSlot: null,
loadingSlots: false,
loading: false,
errorMsg: '',
customerName: '',
customerEmail: '',
customerPhone: '',
showSheet: false,
checkoutUrl: '',
services: @js($services),
async loadSlots() {
if (!this.date) return;
this.loadingSlots = true;
this.selectedSlot = null;
this.slots = [];
try {
const res = await fetch(@js($slotsUrl) + '?date=' + encodeURIComponent(this.date) + '&service_index=' + this.serviceIndex);
const data = await res.json();
this.slots = data.slots || [];
} catch (e) {
this.errorMsg = 'Could not load times. Try again.';
}
this.loadingSlots = false;
},
async submitBooking() {
if (!this.customerName.trim()) { this.errorMsg = 'Enter your full name.'; return; }
if (!this.customerEmail.includes('@')) { this.errorMsg = 'Enter a valid email.'; return; }
if (!this.selectedSlot) { this.errorMsg = 'Pick a time slot.'; return; }
this.loading = true; this.errorMsg = '';
try {
const res = await fetch(@js($bookUrl), {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': @js($csrfToken) },
body: JSON.stringify({
customer_name: this.customerName,
customer_email: this.customerEmail,
customer_phone: this.customerPhone,
service_index: this.serviceIndex,
starts_at: this.selectedSlot.starts_at,
}),
});
const data = await res.json();
if (data.error) { this.errorMsg = data.error; this.loading = false; return; }
if (data.confirmed && data.redirect_url) {
window.location.href = data.redirect_url;
return;
}
if (window.innerWidth < 768) {
this.checkoutUrl = data.checkout_url;
this.showSheet = true;
this.loading = false;
} else {
window.location.href = data.checkout_url;
}
} catch (e) {
this.errorMsg = 'Network error. Please try again.';
this.loading = false;
}
}
}" class="mx-auto max-w-lg min-h-screen bg-white shadow-sm">
<div class="border-b border-slate-100 px-6 py-8 text-center" style="background: linear-gradient(135deg, {{ $brand }}15, #fff)">
@if($hasLogo)
<img src="{{ route('qr.public.menu.logo', $qrCode->short_code) }}" alt="" class="mx-auto h-14 w-14 rounded-2xl object-cover ring-2 ring-white shadow">
@endif
<h1 class="mt-4 text-2xl font-bold text-slate-900">{{ $title }}</h1>
@if(!empty($c['description']))
<p class="mt-2 text-sm text-slate-600">{{ $c['description'] }}</p>
@endif
</div>
<div class="space-y-6 p-6">
<div>
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">1. Service</p>
<div class="mt-3 space-y-2">
<template x-for="(svc, idx) in services" :key="idx">
<button type="button" @click="serviceIndex = idx; date = ''; slots = []; selectedSlot = null"
class="flex w-full items-center justify-between rounded-2xl border px-4 py-3 text-left transition"
:class="serviceIndex === idx ? 'border-indigo-300 bg-indigo-50' : 'border-slate-200 hover:border-slate-300'">
<div>
<p class="font-semibold text-slate-900" x-text="svc.name"></p>
<p class="text-xs text-slate-500" x-text="svc.duration_minutes + ' min' + (svc.description ? ' · ' + svc.description : '')"></p>
</div>
<p class="text-sm font-bold text-slate-900" x-text="svc.price_ghs > 0 ? 'GHS ' + Number(svc.price_ghs).toFixed(2) : 'Free'"></p>
</button>
</template>
</div>
</div>
<div>
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">2. Date</p>
<input type="date" x-model="date" @change="loadSlots()" :min="new Date().toISOString().slice(0,10)"
class="mt-3 w-full rounded-xl border border-slate-200 px-4 py-3 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
</div>
<div x-show="date" x-cloak>
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">3. Time</p>
<p x-show="loadingSlots" class="mt-3 text-sm text-slate-500">Loading times…</p>
<p x-show="!loadingSlots && slots.length === 0 && date" class="mt-3 text-sm text-slate-500">No slots available this day.</p>
<div class="mt-3 grid grid-cols-3 gap-2">
<template x-for="slot in slots" :key="slot.starts_at">
<button type="button" @click="selectedSlot = slot"
class="rounded-xl border px-2 py-2.5 text-sm font-medium transition"
:class="selectedSlot?.starts_at === slot.starts_at ? 'border-indigo-400 bg-indigo-50 text-indigo-800' : 'border-slate-200 text-slate-700 hover:border-slate-300'"
x-text="slot.label"></button>
</template>
</div>
</div>
<div x-show="selectedSlot" x-cloak class="space-y-3">
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">4. Your details</p>
<input type="text" x-model="customerName" placeholder="Full name *" class="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm">
<input type="email" x-model="customerEmail" placeholder="Email *" class="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm">
<input type="tel" x-model="customerPhone" placeholder="Phone (optional)" class="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm">
<div x-show="errorMsg" x-cloak class="rounded-xl bg-red-50 px-4 py-3 text-sm text-red-600" x-text="errorMsg"></div>
<button type="button" @click="submitBooking()" :disabled="loading"
class="w-full rounded-2xl py-4 text-base font-bold text-white transition disabled:opacity-60"
style="background: {{ $brand }}">
<span x-text="loading ? 'Processing…' : 'Confirm booking'">Confirm booking</span>
</button>
</div>
</div>
@include('partials.paystack-sheet')
</div>
</body>
</html>