Files
ladill-merchant/resources/views/public/qr/storefront.blade.php
T
isaacclad c11cc99d17
Deploy Ladill Merchant / deploy (push) Successful in 1m5s
Present Paystack checkout in a responsive mobile sheet and desktop modal.
Standardize customer-facing Ladill Pay checkout shells across products, open them on all viewports, and escape iframe callbacks back to the product flow.
2026-07-21 16:50:07 +00:00

212 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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: '',
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;
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.showSheet = true;
this.loading = false;
} catch (e) {
this.errorMsg = 'Network error. Please try again.';
this.loading = false;
}
},
};
}
</script>
</body>
</html>