Deploy Ladill Merchant / deploy (push) Successful in 59s
Drop required buyer email on public merchant forms; require phone for bookings so merchants can reach guests without Ladill accounts. Co-authored-by: Cursor <cursoragent@cursor.com>
248 lines
13 KiB
PHP
248 lines
13 KiB
PHP
@php
|
|
$c = $qrCode->content();
|
|
$title = $c['book_title'] ?? 'Book';
|
|
$author = $c['author'] ?? '';
|
|
$desc = $c['description'] ?? '';
|
|
$price = number_format((float) ($c['price_ghs'] ?? 0), 2);
|
|
$fileType = strtoupper($c['file_type'] ?? 'PDF');
|
|
$hasCover = !empty($c['cover_path']);
|
|
$orderUrl = $qrCode->publicPath('book/order');
|
|
$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">
|
|
|
|
<div x-data="{
|
|
showModal: false,
|
|
showSheet: false,
|
|
checkoutUrl: '',
|
|
loading: false,
|
|
errorMsg: '',
|
|
customerName: '',
|
|
customerEmail: '',
|
|
customerPhone: '',
|
|
async submitOrder() {
|
|
if (!this.customerName.trim()) { this.errorMsg = 'Enter your full name.'; return; }
|
|
if (!this.customerPhone.trim()) { this.errorMsg = 'Enter your phone number.'; return; }
|
|
this.loading = true; this.errorMsg = '';
|
|
try {
|
|
const res = await fetch(@js($orderUrl), {
|
|
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 }),
|
|
});
|
|
const data = await res.json();
|
|
if (data.error) { this.errorMsg = data.error; this.loading = false; return; }
|
|
if (window.innerWidth < 768) {
|
|
this.showModal = false;
|
|
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="min-h-screen lg:grid lg:grid-cols-2">
|
|
|
|
{{-- Left column: cover panel (desktop only) --}}
|
|
<div class="hidden lg:flex lg:flex-col lg:items-center lg:justify-center bg-white border-r border-slate-200 px-12 py-16">
|
|
@if($hasCover)
|
|
<img src="{{ $qrCode->publicPath('cover') }}"
|
|
alt="{{ $title }} cover"
|
|
class="w-56 rounded-2xl object-cover shadow-xl ring-1 ring-slate-200">
|
|
@else
|
|
<div class="w-56 aspect-[3/4] rounded-2xl bg-slate-100 border border-slate-200 flex items-center justify-center shadow-sm">
|
|
<svg class="h-20 w-20 text-slate-300" fill="none" stroke="currentColor" stroke-width="1.2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"/>
|
|
</svg>
|
|
</div>
|
|
@endif
|
|
<h2 class="mt-8 text-xl font-bold text-slate-900 text-center leading-snug">{{ $title }}</h2>
|
|
@if($author)
|
|
<p class="mt-2 text-sm text-slate-500 text-center">by {{ $author }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Right column: main content --}}
|
|
<div class="bg-slate-50 flex flex-col min-h-screen lg:min-h-0">
|
|
|
|
{{-- Mobile cover header --}}
|
|
<div class="lg:hidden bg-white border-b border-slate-200 px-6 pt-10 pb-8 flex flex-col items-center text-center">
|
|
@if($hasCover)
|
|
<img src="{{ $qrCode->publicPath('cover') }}"
|
|
alt="{{ $title }} cover"
|
|
class="h-48 w-32 rounded-2xl object-cover shadow-xl ring-1 ring-slate-200">
|
|
@else
|
|
<div class="h-48 w-32 rounded-2xl bg-slate-100 border border-slate-200 flex items-center justify-center shadow-sm">
|
|
<svg class="h-16 w-16 text-slate-300" fill="none" stroke="currentColor" stroke-width="1.2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"/>
|
|
</svg>
|
|
</div>
|
|
@endif
|
|
<h1 class="mt-5 text-2xl font-bold text-slate-900 leading-snug">{{ $title }}</h1>
|
|
@if($author)
|
|
<p class="mt-1.5 text-sm font-medium text-slate-500">by {{ $author }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Main content --}}
|
|
<div class="flex-1 p-8 lg:p-12 flex flex-col justify-center bg-white lg:bg-transparent">
|
|
|
|
@if(session('error') || $errors->any())
|
|
<div class="mb-6 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
|
{{ session('error') ?: $errors->first('error') ?: $errors->first() }}
|
|
</div>
|
|
@endif
|
|
|
|
{{-- File type badge --}}
|
|
<div class="mb-4">
|
|
<span class="inline-block rounded-full {{ $fileType === 'EPUB' ? 'bg-indigo-100 text-indigo-700' : 'bg-amber-100 text-amber-700' }} px-3 py-1 text-xs font-bold tracking-wide">{{ $fileType }}</span>
|
|
</div>
|
|
|
|
{{-- Desktop title --}}
|
|
<h1 class="hidden lg:block text-4xl font-bold text-slate-900 leading-tight">{{ $title }}</h1>
|
|
@if($author)
|
|
<p class="hidden lg:block mt-2 text-base text-slate-500">by <span class="font-medium text-slate-700">{{ $author }}</span></p>
|
|
@endif
|
|
|
|
<hr class="my-6 border-slate-100">
|
|
|
|
@if($desc)
|
|
<p class="text-sm text-slate-600 leading-relaxed mb-6">{{ $desc }}</p>
|
|
@endif
|
|
|
|
{{-- Price --}}
|
|
<div class="mb-2">
|
|
<span class="text-sm text-slate-400">GHS</span>
|
|
<span class="text-4xl font-black text-slate-900 ml-1">{{ $price }}</span>
|
|
</div>
|
|
<p class="mb-8 text-xs text-slate-400">Pay once. Download instantly.</p>
|
|
|
|
{{-- Buy button --}}
|
|
<button type="button"
|
|
@click="showModal = true"
|
|
class="flex w-full items-center justify-center gap-2 rounded-2xl bg-amber-500 hover:bg-amber-400 text-slate-900 font-bold py-4 px-8 text-lg transition-colors">
|
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"/>
|
|
</svg>
|
|
Buy & Download
|
|
</button>
|
|
|
|
</div>
|
|
|
|
@include('partials.paystack-sheet')
|
|
</div>
|
|
|
|
{{-- ===== SHEET 1: Customer details ===== --}}
|
|
<div x-show="showModal" x-cloak
|
|
class="fixed inset-0 z-50 flex items-end justify-center"
|
|
style="background: rgba(0,0,0,0.5); backdrop-filter: blur(3px);"
|
|
x-transition:enter="transition-opacity ease-out duration-200"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition-opacity ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0">
|
|
|
|
<div @click.outside="if(!loading) showModal = false"
|
|
class="w-full max-w-lg overflow-hidden rounded-t-3xl bg-white shadow-2xl"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="translate-y-full"
|
|
x-transition:enter-end="translate-y-0"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="translate-y-0"
|
|
x-transition:leave-end="translate-y-full"
|
|
style="max-height: 90vh; overflow-y: auto; padding-bottom: env(safe-area-inset-bottom, 0px)">
|
|
|
|
{{-- Drag handle --}}
|
|
<div class="flex justify-center pt-3 pb-2">
|
|
<div class="h-1 w-10 rounded-full bg-slate-200"></div>
|
|
</div>
|
|
|
|
{{-- Header --}}
|
|
<div class="flex items-center justify-between border-b border-slate-100 px-5 pb-4 pt-2">
|
|
<div>
|
|
<h3 class="text-lg font-bold text-slate-900">Complete purchase</h3>
|
|
<p class="text-xs text-slate-400 mt-0.5">{{ $title }}</p>
|
|
</div>
|
|
<button type="button" @click="if(!loading) showModal = false"
|
|
class="flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 transition">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="px-5 pb-6 pt-5 space-y-5">
|
|
|
|
{{-- Order summary --}}
|
|
<div class="rounded-2xl bg-slate-50 px-4 py-3 flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-xl {{ $fileType === 'EPUB' ? 'bg-indigo-100' : 'bg-amber-100' }}">
|
|
<svg class="h-5 w-5 {{ $fileType === 'EPUB' ? 'text-indigo-600' : 'text-amber-600' }}" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-semibold text-slate-900 leading-snug">{{ $title }}</p>
|
|
<p class="text-xs text-slate-400">{{ $fileType }}{{ $author ? ' · by ' . $author : '' }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-right shrink-0 ml-3">
|
|
<p class="text-[10px] text-slate-400">GHS</p>
|
|
<p class="text-lg font-black text-slate-900">{{ $price }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Contact fields --}}
|
|
<div class="space-y-2.5">
|
|
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Your details</p>
|
|
<input type="text" x-model="customerName"
|
|
autocomplete="name" placeholder="Full name *"
|
|
class="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm placeholder:text-slate-400 focus:border-amber-400 focus:outline-none focus:ring-1 focus:ring-amber-400/30">
|
|
<input type="tel" x-model="customerPhone"
|
|
autocomplete="tel" placeholder="Phone number *"
|
|
class="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm placeholder:text-slate-400 focus:border-amber-400 focus:outline-none focus:ring-1 focus:ring-amber-400/30">
|
|
</div>
|
|
|
|
{{-- Error --}}
|
|
<div x-show="errorMsg" x-cloak
|
|
class="rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-600"
|
|
x-text="errorMsg"></div>
|
|
|
|
{{-- Pay button --}}
|
|
<button type="button"
|
|
@click="submitOrder()"
|
|
:disabled="loading"
|
|
class="flex w-full items-center justify-center gap-2 rounded-2xl bg-amber-500 hover:bg-amber-400 disabled:opacity-60 text-slate-900 font-bold py-4 text-base transition-colors">
|
|
<svg x-show="loading" x-cloak class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
</svg>
|
|
<span x-text="loading ? 'Processing…' : 'Pay GHS {{ $price }} →'">Pay GHS {{ $price }} →</span>
|
|
</button>
|
|
|
|
<p class="text-center text-[11px] text-slate-400">Secured by Paystack. Powered by Ladill Pay</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>{{-- end x-data --}}
|
|
|
|
</body>
|
|
</html>
|