De-fork POS chrome/assets and import CRM products only
Deploy Ladill POS / deploy (push) Successful in 32s
Deploy Ladill POS / deploy (push) Successful in 32s
Finish removing the upstream platform fork: drop the Hosting/SSL/Domain notification classes + their mail views, the email/mailbox/domains/hosting view trees, the Mini/Afia/search/paystack partials and unused components, and the now-orphan support helpers (DomainConfig, MailboxPricing, ResellerClubLegacy), SmsService, and fork configs (afia, hosting, domain, email, mailbox, qr, mail_brands, notifications, ...). Reparent the notifications page off the hosting layout onto the POS app layout (it had referenced undefined hosting./mini. routes). Computed the removable set from a reachability scan of the POS view graph; kept views reference no removed routes/services. Branding: favicon now uses the updated favicon.ico (favicon.svg removed); signed-out fallback logo points at the POS logo. CRM import: constrain the catalogue pull to type=product so CRM services are never imported as POS products (server-side filter + defensive client skip). Test suite green (8 passed, 29 assertions); all routes resolve and every remaining Blade compiles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
46b5091b1e
commit
b7c7a32ee6
@@ -1,17 +0,0 @@
|
||||
@props([
|
||||
'href' => null,
|
||||
'type' => 'button',
|
||||
])
|
||||
|
||||
@php
|
||||
$tag = $href ? 'a' : 'button';
|
||||
@endphp
|
||||
|
||||
<{{ $tag }}
|
||||
@if ($href) href="{{ $href }}" @endif
|
||||
@if ($tag === 'button') type="{{ $type }}" @endif
|
||||
{{ $attributes->class(['btn-primary']) }}
|
||||
>
|
||||
<svg class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||
{{ $slot }}
|
||||
</{{ $tag }}>
|
||||
@@ -1,16 +0,0 @@
|
||||
@props([
|
||||
'href' => null,
|
||||
'type' => 'button',
|
||||
])
|
||||
|
||||
@php
|
||||
$tag = $href ? 'a' : 'button';
|
||||
@endphp
|
||||
|
||||
<{{ $tag }}
|
||||
@if ($href) href="{{ $href }}" @endif
|
||||
@if ($tag === 'button') type="{{ $type }}" @endif
|
||||
{{ $attributes->class(['btn-primary']) }}
|
||||
>
|
||||
{{ $slot }}
|
||||
</{{ $tag }}>
|
||||
@@ -1,69 +0,0 @@
|
||||
@props([
|
||||
'name',
|
||||
'title',
|
||||
'message' => null,
|
||||
'action',
|
||||
'method' => 'POST',
|
||||
'confirmLabel' => 'Confirm',
|
||||
'cancelLabel' => 'Cancel',
|
||||
'variant' => 'danger',
|
||||
])
|
||||
|
||||
@php
|
||||
$confirmBtnClass = $variant === 'danger'
|
||||
? 'bg-red-600 hover:bg-red-700'
|
||||
: 'bg-violet-600 hover:bg-violet-700';
|
||||
$iconWrapClass = $variant === 'danger'
|
||||
? 'bg-red-100 text-red-600'
|
||||
: 'bg-violet-100 text-violet-600';
|
||||
@endphp
|
||||
|
||||
@if(isset($trigger))
|
||||
<span @click.stop="$dispatch('open-modal', {{ Js::from($name) }})" class="inline-flex">
|
||||
{{ $trigger }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
<x-modal :name="$name" maxWidth="md">
|
||||
<div class="px-5 pb-6 pt-2 sm:px-6 sm:pb-6 sm:pt-4">
|
||||
<div class="flex flex-col items-center text-center sm:items-start sm:text-left">
|
||||
<div @class(['flex h-12 w-12 items-center justify-center rounded-full', $iconWrapClass])>
|
||||
@if($variant === 'danger')
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/>
|
||||
</svg>
|
||||
@else
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"/>
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
<h2 class="mt-4 text-lg font-semibold text-slate-900">{{ $title }}</h2>
|
||||
@if($message)
|
||||
<p class="mt-2 text-sm leading-relaxed text-slate-500">{{ $message }}</p>
|
||||
@endif
|
||||
@isset($details)
|
||||
<div class="mt-3 w-full">{{ $details }}</div>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ $action }}" class="mt-6 flex flex-col-reverse gap-2.5 sm:flex-row sm:justify-end">
|
||||
@csrf
|
||||
@if(strtoupper($method) !== 'POST')
|
||||
@method($method)
|
||||
@endif
|
||||
@isset($fields)
|
||||
{{ $fields }}
|
||||
@endisset
|
||||
<button type="button"
|
||||
@click="$dispatch('close-modal', {{ Js::from($name) }})"
|
||||
class="rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">
|
||||
{{ $cancelLabel }}
|
||||
</button>
|
||||
<button type="submit"
|
||||
@class(['rounded-xl px-4 py-2.5 text-sm font-semibold text-white transition', $confirmBtnClass])>
|
||||
{{ $confirmLabel }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-modal>
|
||||
@@ -1,73 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
@include('partials.favicon')
|
||||
<title>{{ $title ?? 'Hosting Panel' }} - Ladill</title>
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
@php
|
||||
$canViewAccountDetails = auth()->check() && auth()->user()->can('viewAccount', $account);
|
||||
$exitUrl = $canViewAccountDetails
|
||||
? route('hosting.accounts.show', $account)
|
||||
: route('hosting.single-domain');
|
||||
@endphp
|
||||
<body class="bg-slate-100 font-sans antialiased" x-data="{ sidebarOpen: false }">
|
||||
<div class="min-h-screen flex">
|
||||
{{-- Mobile sidebar overlay --}}
|
||||
<div x-show="sidebarOpen" x-cloak class="fixed inset-0 z-30 bg-black/50 lg:hidden" @click="sidebarOpen = false"></div>
|
||||
|
||||
{{-- Sidebar --}}
|
||||
<aside class="fixed inset-y-0 left-0 z-40 w-64 transform transition-transform lg:translate-x-0 bg-white border-r border-slate-200"
|
||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
|
||||
@include('hosting.panel.partials.sidebar')
|
||||
</aside>
|
||||
|
||||
{{-- Main content --}}
|
||||
<div class="flex-1 flex flex-col min-w-0 lg:pl-64">
|
||||
{{-- Top bar --}}
|
||||
<header class="sticky top-0 z-20 flex h-14 items-center gap-4 border-b border-slate-200 bg-white/95 backdrop-blur px-4 lg:px-6">
|
||||
<button type="button" @click="sidebarOpen = true" class="lg:hidden -ml-2 p-2 text-slate-500 hover:text-slate-700">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
|
||||
</button>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-sm font-semibold text-slate-800 truncate">{{ $header ?? 'Hosting Panel' }}</h1>
|
||||
@unless ($canViewAccountDetails)
|
||||
<p class="text-xs text-slate-500">Developer access</p>
|
||||
@endunless
|
||||
</div>
|
||||
<a href="{{ $exitUrl }}" class="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-600 hover:bg-slate-50 transition">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
Exit Panel
|
||||
</a>
|
||||
</header>
|
||||
|
||||
{{-- Flash messages --}}
|
||||
@include('partials.flash')
|
||||
|
||||
{{-- Expired Account Banner --}}
|
||||
@if ($account->isExpired() && $account->isInGracePeriod())
|
||||
<div class="mx-4 mt-4 lg:mx-6 lg:mt-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<svg class="h-4.5 w-4.5 shrink-0 text-red-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"/></svg>
|
||||
<p class="text-sm text-red-800"><span class="font-semibold">Account expired.</span> Your site is offline. Files are preserved until {{ $account->gracePeriodEndsAt()->format('M d, Y') }}.
|
||||
<a href="{{ route('hosting.accounts.show', $account) }}" class="underline font-medium hover:text-red-900">Renew now</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Page content --}}
|
||||
<main class="flex-1 p-4 lg:p-6">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
@php
|
||||
$class = $class ?? 'h-5 w-5';
|
||||
@endphp
|
||||
<svg class="{{ $class }}" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="m21 0h-7c-1.657 0-3 1.343-3 3v3h13v-3c0-1.657-1.343-3-3-3zm-6.5 4.5c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm6.5-.5h-2c-.552 0-1-.448-1-1s.448-1 1-1h2c.552 0 1 .448 1 1s-.448 1-1 1zm-10 6c0 1.657 1.343 3 3 3h7c1.657 0 3-1.343 3-3v-3h-13zm8-1h2c.552 0 1 .448 1 1s-.448 1-1 1h-2c-.552 0-1-.448-1-1s.448-1 1-1zm-4.5-.5c.828 0 1.5.672 1.5 1.5s-.672 1.5-1.5 1.5-1.5-.672-1.5-1.5.672-1.5 1.5-1.5zm-5.5-6.5c0 .552-.447 1-1 1h-1c-1.103 0-2 .897-2 2v2.029l1.307-1.25c.399-.383 1.031-.37 1.414.028s.37 1.031-.027 1.414l-2.301 2.212c-.38.378-.883.569-1.388.569-.51 0-1.021-.193-1.41-.582l-2.288-2.199c-.397-.383-.41-1.016-.027-1.414s1.015-.411 1.414-.028l1.307 1.256v-2.035c-.001-2.206 1.793-4 3.999-4h1c.553 0 1 .448 1 1zm12 14v1c0 2.206-1.794 4-4 4h-2.029l1.25 1.307c.383.398.37 1.031-.027 1.414-.398.382-1.031.371-1.414-.028l-2.212-2.301c-.761-.761-.761-2.023.013-2.798l2.199-2.287c.382-.398 1.015-.412 1.414-.028.397.383.41 1.016.027 1.414l-1.257 1.307h2.036c1.103 0 2-.897 2-2v-1c0-.552.447-1 1-1s1 .448 1 1zm-12-1.5v2.421c-.221.268-1.456 1.079-4.5 1.079s-4.279-.811-4.5-1.079v-2.421c0-.883 1.85-1.5 4.5-1.5s4.5.617 4.5 1.5zm-4.5 5.5c1.93 0 3.421-.311 4.5-.777v2.61c0 1.377-1.641 2.167-4.5 2.167s-4.5-.79-4.5-2.167v-2.61c1.079.466 2.57.777 4.5.777z"/>
|
||||
</svg>
|
||||
@@ -1,5 +0,0 @@
|
||||
@props(['class' => 'h-5 w-5'])
|
||||
|
||||
<svg {{ $attributes->merge(['class' => $class]) }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="m4,11h7v-7h-7v7Zm2-5h3v3h-3v-3Zm14-2h-7v7h7v-7Zm-2,5h-3v-3h3v3Zm-14,11h7v-7h-7v7Zm2-5h3v3h-3v-3Zm-3,7h4v2H3c-1.654,0-3-1.346-3-3v-4h2v4c0,.551.449,1,1,1Zm19-5h2v4c0,1.654-1.346,3-3,3h-4v-2h4c.551,0,1-.449,1-1v-4Zm2-14v4h-2V3c0-.551-.449-1-1-1h-4V0h4c1.654,0,3,1.346,3,3ZM2,7H0V3C0,1.346,1.346,0,3,0h4v2H3c-.551,0-1,.449-1,1v4Zm11,10h3v3h-3v-3Zm4-1v-3h3v3h-3Zm-4-3h3v3h-3v-3Z"/>
|
||||
</svg>
|
||||
@@ -1,7 +0,0 @@
|
||||
@php
|
||||
$class = $class ?? 'h-5 w-5';
|
||||
$strokeWidth = $strokeWidth ?? '1.5';
|
||||
@endphp
|
||||
<svg class="{{ $class }}" fill="none" stroke="currentColor" stroke-width="{{ $strokeWidth }}" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7"/>
|
||||
</svg>
|
||||
@@ -1,6 +0,0 @@
|
||||
@php
|
||||
$class = $class ?? 'h-5 w-5';
|
||||
@endphp
|
||||
<svg class="{{ $class }}" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="m24 12c0 6.617-5.383 12-12 12-3.476 0-6.744-1.542-9-4.104v2.104c0 .552-.447 1-1 1s-1-.448-1-1v-4c0-1.103.897-2 2-2h4c.553 0 1 .448 1 1s-.447 1-1 1h-2.991c1.877 2.49 4.837 4 7.991 4 5.514 0 10-4.486 10-10 0-.552.447-1 1-1s1 .448 1 1zm-22 0c0-5.514 4.486-10 10-10 3.154 0 6.115 1.51 7.991 4h-2.991c-.553 0-1 .448-1 1s.447 1 1 1h4c1.103 0 2-.897 2-2v-4c0-.552-.447-1-1-1s-1 .448-1 1v2.104c-2.256-2.562-5.524-4.104-9-4.104-6.617 0-12 5.383-12 12 0 .552.447 1 1 1s1-.448 1-1zm13.5-3.5c1.93 0 3.5 1.57 3.5 3.5s-1.57 3.5-3.5 3.5c-1.386 0-2.685-1.085-3.5-1.949-.815.864-2.114 1.949-3.5 1.949-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5c1.386 0 2.685 1.085 3.5 1.949.815-.864 2.114-1.949 3.5-1.949zm-4.795 3.5c-.67-.736-1.591-1.5-2.205-1.5-.827 0-1.5.673-1.5 1.5s.673 1.5 1.5 1.5c.614 0 1.535-.764 2.205-1.5zm4.795-1.5c-.614 0-1.535.764-2.205 1.5.67.736 1.591 1.5 2.205 1.5.827 0 1.5-.673 1.5-1.5s-.673-1.5-1.5-1.5z"/>
|
||||
</svg>
|
||||
@@ -1,9 +0,0 @@
|
||||
@props(['messages'])
|
||||
|
||||
@if ($messages)
|
||||
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
|
||||
@foreach ((array) $messages as $message)
|
||||
<li>{{ $message }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@@ -1,32 +0,0 @@
|
||||
@props([
|
||||
'title',
|
||||
'subtitle' => null,
|
||||
'backUrl' => null,
|
||||
'badge' => null,
|
||||
'hideAfia' => false,
|
||||
])
|
||||
|
||||
<div class="sticky top-0 z-20 border-b border-slate-200 bg-white/95 backdrop-blur lg:hidden">
|
||||
<div class="flex items-center gap-3 px-4 py-3">
|
||||
@if ($backUrl)
|
||||
<a href="{{ $backUrl }}"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 text-slate-600 transition hover:bg-slate-50">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
</a>
|
||||
@endif
|
||||
<div class="min-w-0 flex-1">
|
||||
@if ($subtitle)
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">{{ $subtitle }}</p>
|
||||
@endif
|
||||
<h1 class="truncate text-base font-semibold text-slate-900">{{ $title }}</h1>
|
||||
</div>
|
||||
@if ($badge)
|
||||
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-600">{{ $badge }}</span>
|
||||
@endif
|
||||
@unless ($hideAfia)
|
||||
@include('partials.afia-button', ['compact' => true])
|
||||
@endunless
|
||||
{{ $actions ?? '' }}
|
||||
</div>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@@ -1,88 +0,0 @@
|
||||
@props([
|
||||
'name',
|
||||
'show' => false,
|
||||
'maxWidth' => '2xl'
|
||||
])
|
||||
|
||||
@php
|
||||
$maxWidth = [
|
||||
'sm' => 'sm:max-w-sm',
|
||||
'md' => 'sm:max-w-md',
|
||||
'lg' => 'sm:max-w-lg',
|
||||
'xl' => 'sm:max-w-xl',
|
||||
'2xl' => 'sm:max-w-2xl',
|
||||
][$maxWidth];
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
show: @js($show),
|
||||
focusables() {
|
||||
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
||||
return [...$el.querySelectorAll(selector)]
|
||||
.filter(el => ! el.hasAttribute('disabled'))
|
||||
},
|
||||
firstFocusable() { return this.focusables()[0] },
|
||||
lastFocusable() { return this.focusables().slice(-1)[0] },
|
||||
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
|
||||
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
||||
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
||||
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
||||
}"
|
||||
x-init="$watch('show', value => {
|
||||
if (value) {
|
||||
document.body.classList.add('overflow-y-hidden');
|
||||
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
|
||||
} else {
|
||||
setTimeout(() => { if (!show) document.body.classList.remove('overflow-y-hidden'); }, 200);
|
||||
}
|
||||
})"
|
||||
x-on:open-modal.window="String($event.detail) === @js($name) ? show = true : null"
|
||||
data-modal-name="{{ $name }}"
|
||||
x-on:close-modal.window="String($event.detail) === @js($name) ? show = false : null"
|
||||
x-on:close.stop="show = false"
|
||||
x-on:keydown.escape.window="show = false"
|
||||
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||
x-show="show"
|
||||
x-cloak
|
||||
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"
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center sm:justify-center"
|
||||
style="background: rgba(0,0,0,0.45); backdrop-filter: blur(3px);"
|
||||
>
|
||||
{{-- Backdrop click to close --}}
|
||||
<div class="absolute inset-0" x-on:click="show = false" data-close-modal="{{ $name }}"></div>
|
||||
|
||||
{{-- Panel: bottom-sheet on mobile, centered card on sm+ --}}
|
||||
<div
|
||||
x-show="show"
|
||||
class="relative z-10 w-full max-h-[90vh] overflow-y-auto rounded-t-3xl bg-white shadow-2xl sm:mb-6 sm:max-h-[85vh] sm:rounded-2xl {{ $maxWidth }} sm:border sm:border-gray-200"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
|
||||
x-transition:enter-end="translate-y-0 sm:opacity-100 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="translate-y-0 sm:opacity-100 sm:scale-100"
|
||||
x-transition:leave-end="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
|
||||
x-on:click.stop
|
||||
>
|
||||
{{-- Drag handle (mobile only) --}}
|
||||
<div class="flex justify-center pb-1 pt-3 sm:hidden">
|
||||
<div class="h-1 w-10 rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
|
||||
{{-- Close button (desktop only) --}}
|
||||
<button @click="show = false" data-close-modal="{{ $name }}" type="button"
|
||||
class="absolute right-3 top-3 z-10 hidden h-8 w-8 items-center justify-center rounded-full text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 sm:flex">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,33 +0,0 @@
|
||||
@props(['title' => 'Ladill Hosting'])
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-slate-100">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>{{ $title ?? 'Ladill Hosting' }}</title>
|
||||
@include('partials.favicon')
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="h-full font-sans antialiased bg-slate-100" x-data="{ sidebarOpen: false }">
|
||||
<div class="min-h-screen max-lg:min-h-dvh">
|
||||
<div x-show="sidebarOpen" x-cloak class="fixed inset-0 z-30 bg-black/50 lg:hidden" @click="sidebarOpen = false"></div>
|
||||
<aside x-cloak class="fixed inset-y-0 left-0 z-40 w-64 transform transition-transform lg:translate-x-0"
|
||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
|
||||
@include('partials.sidebar')
|
||||
</aside>
|
||||
<div class="flex min-h-screen flex-col min-w-0 lg:pl-64">
|
||||
@include('partials.topbar')
|
||||
<main class="flex-1 p-6 pb-24 lg:p-6 lg:pb-6">
|
||||
@include('partials.flash')
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@auth
|
||||
@include('partials.afia')
|
||||
@endauth
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,124 +0,0 @@
|
||||
@props([
|
||||
'id',
|
||||
'title' => 'Add credits',
|
||||
'description' => '',
|
||||
'topupAction',
|
||||
'minTopup' => 5,
|
||||
'suggestedAmount' => 10,
|
||||
'ladillWalletBalance' => 0,
|
||||
'serviceBalance' => null,
|
||||
'serviceBalanceLabel' => 'Current balance',
|
||||
'openOnLoad' => false,
|
||||
'returnUrl' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
// Single-wallet (siloing step 2): there is one Ladill wallet, so "pay from
|
||||
// wallet into service credits" is a meaningless round-trip — top up the one
|
||||
// wallet directly (Paystack). Transfer option removed.
|
||||
$canPayFromWallet = false;
|
||||
@endphp
|
||||
|
||||
<x-modal :name="$id" :show="$openOnLoad" maxWidth="md">
|
||||
<div class="flex min-h-full flex-col sm:min-h-0">
|
||||
<div class="border-b border-slate-100 px-5 py-4 pr-12 sm:px-6 sm:pr-14">
|
||||
<h2 class="text-lg font-semibold text-slate-900">{{ $title }}</h2>
|
||||
@if($description)
|
||||
<p class="mt-1 text-sm text-slate-500">{{ $description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form action="{{ $topupAction }}" method="POST" class="flex flex-1 flex-col p-5 sm:p-6"
|
||||
x-data="{
|
||||
method: 'paystack',
|
||||
showSheet: false,
|
||||
checkoutUrl: '',
|
||||
loading: false,
|
||||
errorMsg: '',
|
||||
async submitTopup(event) {
|
||||
if (this.method !== 'paystack' || window.innerWidth >= 768) return;
|
||||
|
||||
event.preventDefault();
|
||||
this.loading = true;
|
||||
this.errorMsg = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(event.target.action, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
body: new FormData(event.target),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || data.error || !data.checkout_url) {
|
||||
this.errorMsg = data.error || 'Unable to start payment. Please try again.';
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkoutUrl = data.checkout_url;
|
||||
this.showSheet = true;
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
this.errorMsg = 'Network error. Please try again.';
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
}"
|
||||
@submit="submitTopup($event)">
|
||||
@csrf
|
||||
@if($returnUrl)
|
||||
<input type="hidden" name="return_url" value="{{ $returnUrl }}">
|
||||
@endif
|
||||
|
||||
@if($serviceBalance !== null)
|
||||
<div class="mb-4 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-center">
|
||||
<p class="text-xl font-bold text-slate-900">GHS {{ number_format((float) $serviceBalance, 2) }}</p>
|
||||
<p class="text-xs text-slate-500">{{ $serviceBalanceLabel }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-show="errorMsg" x-cloak class="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700" x-text="errorMsg"></div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-medium text-slate-700">Amount (GHS)</label>
|
||||
<input type="number" name="amount" min="{{ $minTopup }}" max="5000" step="0.01"
|
||||
value="{{ $suggestedAmount }}"
|
||||
class="mt-1.5 block w-full rounded-xl border border-slate-200 px-3 py-2.5 text-sm focus:border-indigo-400 focus:ring-1 focus:ring-indigo-400"
|
||||
required>
|
||||
<p class="mt-1.5 text-xs text-slate-500">Minimum GHS {{ number_format($minTopup, 2) }}</p>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="payment_method" value="paystack" @if($canPayFromWallet) x-model="method" @endif>
|
||||
|
||||
@if($canPayFromWallet)
|
||||
<div class="mt-4">
|
||||
<p class="mb-2 text-xs font-medium text-slate-600">Payment method</p>
|
||||
<div class="flex rounded-xl border border-slate-200 bg-white p-0.5 text-sm">
|
||||
<button type="button" @click="method = 'paystack'"
|
||||
:class="method === 'paystack' ? 'bg-indigo-600 text-white' : 'text-slate-600'"
|
||||
class="flex-1 rounded-lg py-2 font-medium transition">Paystack</button>
|
||||
<button type="button" @click="method = 'wallet'"
|
||||
:class="method === 'wallet' ? 'bg-indigo-600 text-white' : 'text-slate-600'"
|
||||
class="flex-1 rounded-lg py-2 font-medium transition">Wallet (GHS {{ number_format($ladillWalletBalance, 2) }})</button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<p class="mt-4 text-xs text-slate-500">Pay with Paystack.</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 flex flex-col gap-2 sm:mt-auto sm:flex-row sm:justify-end">
|
||||
<button type="button" @click="$dispatch('close-modal', '{{ $id }}')"
|
||||
data-close-modal="{{ $id }}"
|
||||
class="hidden rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 sm:inline-flex">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" :disabled="loading"
|
||||
class="btn-primary w-full sm:w-auto">
|
||||
<span x-text="loading ? 'Processing…' : 'Add credits'">Add credits</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@include('partials.paystack-sheet')
|
||||
</form>
|
||||
</div>
|
||||
</x-modal>
|
||||
Reference in New Issue
Block a user