Deploy Ladill Merchant / deploy (push) Successful in 38s
Use the shared Inline sheet, hold busy until payment UI opens, and avoid locking page scroll during the handoff.
249 lines
14 KiB
PHP
249 lines
14 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: '',
|
|
accessCode: '',
|
|
publicKey: '',
|
|
returnUrl: '',
|
|
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; window.LadillPayCheckout?.prepare?.(); 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) { window.LadillPayCheckout?.cancel?.(); this.errorMsg = data.error; this.loading = false; return; }
|
|
this.showModal = false;
|
|
this.checkoutUrl = data.checkout_url;
|
|
this.accessCode = data.access_code || '';
|
|
this.publicKey = data.public_key || '';
|
|
this.returnUrl = data.callback_url || '';
|
|
this.showSheet = true; // loading cleared when payment UI opens (ladill-pay-opened)
|
|
} catch(e) {
|
|
window.LadillPayCheckout?.cancel?.(); 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>
|