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.
217 lines
12 KiB
PHP
217 lines
12 KiB
PHP
@php
|
||
$c = $qrCode->content();
|
||
$sections = $c['sections'] ?? [];
|
||
$title = $c['title'] ?? $qrCode->label ?? 'Storefront';
|
||
$cur = $c['currency'] ?? 'GHS';
|
||
$brand = $c['brand_color'] ?? '#4f46e5';
|
||
$acceptsPayment = ! empty($c['accepts_payment']);
|
||
$shippingType = $c['shipping_type'] ?? 'none';
|
||
$shippingFee = (float) ($c['shipping_fee'] ?? 0);
|
||
$freeAbove = (float) ($c['free_shipping_above'] ?? 0);
|
||
$isShop = $qrCode->type === \App\Models\QrCode::TYPE_SHOP;
|
||
$hasLogo = ! empty($c['logo_path']);
|
||
$hasCover = ! empty($c['cover_path']);
|
||
$logoUrl = $hasLogo ? $qrCode->publicPath('menu-logo') : null;
|
||
$coverUrl = $hasCover ? $qrCode->publicPath('menu-cover') : null;
|
||
$orderUrl = $qrCode->publicPath('order');
|
||
@endphp
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<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>body{font-family:'Figtree',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}[x-cloak]{display:none!important}</style>
|
||
</head>
|
||
<body class="bg-slate-50 text-slate-900 antialiased">
|
||
<div x-data="storefront({
|
||
cur: @js($cur),
|
||
acceptsPayment: {{ $acceptsPayment ? 'true' : 'false' }},
|
||
shippingType: @js($shippingType),
|
||
shippingFee: {{ $shippingFee }},
|
||
freeAbove: {{ $freeAbove }},
|
||
orderUrl: @js($orderUrl),
|
||
csrf: @js(csrf_token()),
|
||
})" class="mx-auto max-w-2xl px-4 pb-32 pt-0">
|
||
@include('partials.paystack-sheet')
|
||
|
||
{{-- Cover + header --}}
|
||
<div class="relative -mx-4 mb-4 h-36 sm:h-44" style="background:{{ $brand }}">
|
||
@if($coverUrl)<img src="{{ $coverUrl }}" alt="" class="h-full w-full object-cover opacity-90">@endif
|
||
</div>
|
||
<div class="relative z-10 -mt-12 flex items-end gap-4 px-1">
|
||
<div class="flex h-20 w-20 shrink-0 items-center justify-center overflow-hidden rounded-2xl bg-white shadow-md ring-4 ring-white" style="color:{{ $brand }}">
|
||
@if($logoUrl)
|
||
<img src="{{ $logoUrl }}" alt="{{ $title }}" class="h-full w-full object-cover">
|
||
@else
|
||
<span class="text-2xl font-black">{{ mb_substr($title, 0, 1) }}</span>
|
||
@endif
|
||
</div>
|
||
<div class="pb-1">
|
||
<h1 class="text-xl font-bold text-slate-900">{{ $title }}</h1>
|
||
<p class="text-xs text-slate-500">{{ $isShop ? 'Shop' : 'Menu' }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
@if(session('order_error'))
|
||
<div class="mt-4 rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">{{ session('order_error') }}</div>
|
||
@endif
|
||
|
||
{{-- Catalogue --}}
|
||
<div class="mt-6 space-y-7">
|
||
@forelse($sections as $section)
|
||
<div>
|
||
<h2 class="mb-3 text-sm font-bold uppercase tracking-wide text-slate-500">{{ $section['name'] ?? '' }}</h2>
|
||
<div class="space-y-3">
|
||
@foreach(($section['items'] ?? []) as $iIdx => $item)
|
||
@php
|
||
$price = (float) ($item['price'] ?? 0);
|
||
$key = ($section['name'] ?? 's').'-'.$iIdx;
|
||
$img = ! empty($item['image_path']);
|
||
@endphp
|
||
<div class="flex items-center gap-3 rounded-2xl border border-slate-200 bg-white p-3">
|
||
@if($img)
|
||
<img src="{{ $qrCode->publicPath('item-image/'.$loop->parent->index.'/'.$iIdx) }}" alt="" class="h-16 w-16 shrink-0 rounded-xl object-cover">
|
||
@endif
|
||
<div class="min-w-0 flex-1">
|
||
<p class="truncate text-sm font-semibold text-slate-900">{{ $item['name'] ?? '' }}</p>
|
||
@if(!empty($item['description']))<p class="truncate text-xs text-slate-500">{{ $item['description'] }}</p>@endif
|
||
<p class="mt-0.5 text-sm font-bold" style="color:{{ $brand }}">{{ $cur }} {{ number_format($price, 2) }}</p>
|
||
</div>
|
||
@if($acceptsPayment && $price > 0)
|
||
<button type="button"
|
||
@click="add(@js($item['name'] ?? ''), {{ $price }}, @js($key))"
|
||
class="shrink-0 rounded-xl px-3 py-2 text-xs font-bold text-white shadow-sm" style="background:{{ $brand }}">Add</button>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
@empty
|
||
<p class="rounded-xl border border-slate-200 bg-white px-4 py-8 text-center text-sm text-slate-500">This storefront has no items yet.</p>
|
||
@endforelse
|
||
</div>
|
||
|
||
@if($acceptsPayment)
|
||
{{-- Cart bar --}}
|
||
<div x-show="count > 0" x-cloak class="fixed inset-x-0 bottom-0 z-30 border-t border-slate-200 bg-white/95 backdrop-blur">
|
||
<div class="mx-auto flex max-w-2xl items-center justify-between gap-3 px-4 py-3">
|
||
<div class="text-sm">
|
||
<span class="font-semibold text-slate-900" x-text="count + (count === 1 ? ' item' : ' items')"></span>
|
||
<span class="text-slate-500" x-text="'· ' + @js($cur) + ' ' + total.toFixed(2)"></span>
|
||
</div>
|
||
<button type="button" @click="open = true" class="rounded-xl px-5 py-2.5 text-sm font-bold text-white shadow-sm" style="background:{{ $brand }}">View cart</button>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Cart / checkout sheet --}}
|
||
<div x-show="open" x-cloak class="fixed inset-0 z-40 flex items-end justify-center sm:items-center" @keydown.escape.window="open = false">
|
||
<div class="absolute inset-0 bg-black/40" @click="open = false"></div>
|
||
<div class="relative max-h-[90vh] w-full max-w-md overflow-y-auto rounded-t-3xl bg-white p-5 shadow-xl sm:rounded-3xl">
|
||
<div class="mb-4 flex items-center justify-between">
|
||
<h3 class="text-lg font-bold text-slate-900">Your order</h3>
|
||
<button type="button" @click="open = false" class="text-slate-400 hover:text-slate-600">✕</button>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<template x-for="(line, k) in cart" :key="k">
|
||
<div class="flex items-center gap-2 rounded-xl border border-slate-200 px-3 py-2">
|
||
<div class="min-w-0 flex-1">
|
||
<p class="truncate text-sm font-semibold text-slate-800" x-text="line.name"></p>
|
||
<p class="text-xs text-slate-500" x-text="@js($cur) + ' ' + line.price.toFixed(2)"></p>
|
||
</div>
|
||
<button type="button" @click="dec(k)" class="h-7 w-7 rounded-lg border border-slate-200 text-slate-600">−</button>
|
||
<span class="w-6 text-center text-sm font-semibold" x-text="line.qty"></span>
|
||
<button type="button" @click="inc(k)" class="h-7 w-7 rounded-lg border border-slate-200 text-slate-600">+</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="mt-4 space-y-1 border-t border-slate-100 pt-3 text-sm">
|
||
<div class="flex justify-between text-slate-600"><span>Subtotal</span><span x-text="@js($cur) + ' ' + subtotal.toFixed(2)"></span></div>
|
||
<div x-show="shipping > 0" class="flex justify-between text-slate-600"><span>Delivery</span><span x-text="@js($cur) + ' ' + shipping.toFixed(2)"></span></div>
|
||
<div class="flex justify-between text-base font-bold text-slate-900"><span>Total</span><span x-text="@js($cur) + ' ' + total.toFixed(2)"></span></div>
|
||
</div>
|
||
|
||
<div class="mt-4 space-y-2">
|
||
<input type="text" x-model="name" placeholder="Your name *" class="w-full rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||
<input type="tel" x-model="phone" placeholder="Phone *" class="w-full rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||
</div>
|
||
|
||
<p x-show="errorMsg" x-cloak class="mt-3 rounded-xl border border-red-100 bg-red-50 px-3 py-2 text-sm text-red-600" x-text="errorMsg"></p>
|
||
|
||
<button type="button" @click="checkout()" :disabled="loading"
|
||
class="mt-4 w-full rounded-xl py-3 text-sm font-bold text-white shadow-sm disabled:opacity-60" style="background:{{ $brand }}">
|
||
<span x-text="loading ? 'Processing…' : ('Pay ' + @js($cur) + ' ' + total.toFixed(2))"></span>
|
||
</button>
|
||
<p class="mt-2 text-center text-[11px] text-slate-400">Secure payment · You'll get an order confirmation.</p>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<script>
|
||
function storefront(cfg) {
|
||
return {
|
||
...cfg,
|
||
open: false,
|
||
loading: false,
|
||
errorMsg: '',
|
||
showSheet: false,
|
||
checkoutUrl: '',
|
||
accessCode: '',
|
||
publicKey: '',
|
||
returnUrl: '',
|
||
name: '', email: '', phone: '',
|
||
cart: {},
|
||
add(name, price, key) {
|
||
if (this.cart[key]) { this.cart[key].qty++; }
|
||
else { this.cart[key] = { name, price, qty: 1 }; }
|
||
},
|
||
inc(k) { this.cart[k].qty++; },
|
||
dec(k) { if (--this.cart[k].qty <= 0) delete this.cart[k]; if (Object.keys(this.cart).length === 0) this.open = false; },
|
||
get count() { return Object.values(this.cart).reduce((n, l) => n + l.qty, 0); },
|
||
get subtotal() { return Object.values(this.cart).reduce((s, l) => s + l.price * l.qty, 0); },
|
||
get shipping() {
|
||
if (this.shippingType !== 'flat' || this.subtotal <= 0) return 0;
|
||
if (this.freeAbove > 0 && this.subtotal >= this.freeAbove) return 0;
|
||
return this.shippingFee;
|
||
},
|
||
get total() { return this.subtotal + this.shipping; },
|
||
async checkout() {
|
||
this.errorMsg = '';
|
||
if (!this.name.trim() || !this.phone.trim()) { this.errorMsg = 'Please fill in your name and phone.'; return; }
|
||
if (this.count === 0) { this.errorMsg = 'Your cart is empty.'; return; }
|
||
this.loading = true; window.LadillPayCheckout?.prepare?.();
|
||
try {
|
||
const items = Object.values(this.cart).map(l => ({ name: l.name, price: l.price, qty: l.qty }));
|
||
const res = await fetch(this.orderUrl, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': this.csrf, 'Accept': 'application/json' },
|
||
body: JSON.stringify({ customer_name: this.name, customer_email: this.email, customer_phone: this.phone, shipping_fee: this.shipping, items }),
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) { this.errorMsg = data.error || 'Could not start checkout.'; this.loading = false; return; }
|
||
if (!data.checkout_url) { this.errorMsg = 'Could not start checkout.'; this.loading = false; return; }
|
||
this.open = 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;
|
||
}
|
||
},
|
||
};
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|