Deploy Ladill Mini / deploy (push) Successful in 34s
Wire UserProfileMenu and shared Blade partial into topbar, mobile nav, and layouts so menu items match the account portal (Ladill Mail excluded). Co-authored-by: Cursor <cursoragent@cursor.com>
265 lines
10 KiB
PHP
265 lines
10 KiB
PHP
@php
|
|
$mobileFullScreenPage = request()->routeIs('account.settings')
|
|
|| request()->routeIs('mini.payment-qrs.create')
|
|
|| request()->routeIs('mini.payment-qrs.show');
|
|
|
|
$qrMobilePage = request()->routeIs('mini.payment-qrs.create') || request()->routeIs('mini.payment-qrs.show');
|
|
@endphp
|
|
<!DOCTYPE html>
|
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['qr-mobile-page' => $qrMobilePage])>
|
|
<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() }}">
|
|
@include('partials.favicon')
|
|
<title>{{ $title ?? 'Dashboard' }} - 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>
|
|
<body class="font-sans antialiased bg-slate-100" x-data="{ sidebarOpen: false }">
|
|
<div class="min-h-screen max-lg:min-h-dvh">
|
|
{{-- 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 — full product nav (Bird and other extracted apps have their
|
|
own nav in their own apps). --}}
|
|
@php $sidebarPartial = 'partials.sidebar'; @endphp
|
|
<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($sidebarPartial)
|
|
</aside>
|
|
|
|
{{-- Main content --}}
|
|
<div class="flex min-h-screen flex-col min-w-0 lg:pl-64">
|
|
<div class="{{ $mobileFullScreenPage ? 'hidden lg:block' : '' }}">
|
|
@include('partials.topbar-qr')
|
|
</div>
|
|
|
|
<div @class([
|
|
'px-6 pt-4',
|
|
'hidden lg:block' => $mobileFullScreenPage,
|
|
])>
|
|
@include('partials.flash')
|
|
</div>
|
|
|
|
<main @class([
|
|
'flex-1 lg:p-6 lg:pb-6',
|
|
'px-4 pt-4 max-lg:pb-[calc(4rem+max(env(safe-area-inset-bottom,0px),20px))]' => $qrMobilePage,
|
|
'p-0 pb-24 lg:p-6 lg:pb-6' => $mobileFullScreenPage && ! $qrMobilePage,
|
|
'p-6 pb-24 lg:pb-6' => ! $mobileFullScreenPage,
|
|
])>
|
|
{{ $slot }}
|
|
</main>
|
|
</div>
|
|
|
|
{{-- Mobile Bottom Navigation (hidden on QR create/show which have their own action bar) --}}
|
|
@unless(request()->routeIs('mini.payment-qrs.create') || request()->routeIs('mini.payment-qrs.show'))
|
|
@php
|
|
$navUser = auth()->user();
|
|
$navInitials = collect(explode(' ', trim((string) $navUser?->name)))
|
|
->filter()->take(2)
|
|
->map(fn ($part) => strtoupper(substr($part, 0, 1)))
|
|
->implode('');
|
|
@endphp
|
|
@include('partials.mobile-bottom-nav', [
|
|
'homeUrl' => route('mini.dashboard'),
|
|
'homeActive' => request()->routeIs('mini.dashboard'),
|
|
'searchUrl' => route('mini.search'),
|
|
'searchActive' => request()->routeIs('mini.search'),
|
|
'notificationsUrl' => route('notifications.index'),
|
|
'notificationsActive' => request()->routeIs('notifications.*'),
|
|
'unreadUrl' => route('notifications.unread'),
|
|
'profileActive' => request()->routeIs('account.settings'),
|
|
'profileName' => $navUser?->name ?? '',
|
|
'profileSubtitle' => $navUser?->email ?? '',
|
|
'profileMenuItems' => \App\Support\UserProfileMenu::items($navUser),
|
|
['type' => 'link', 'label' => 'Account Settings', 'href' => ladill_account_url('account-settings')],
|
|
['type' => 'link', 'label' => 'Overview', 'href' => route('mini.dashboard')],
|
|
['type' => 'logout', 'label' => 'Logout', 'action' => route('logout')],
|
|
],
|
|
'avatarUrl' => $navUser?->avatarUrl(),
|
|
'initials' => $navInitials !== '' ? $navInitials : 'U',
|
|
])
|
|
@endunless
|
|
|
|
@if ($qrMobilePage)
|
|
<div class="qr-mobile-bottom-bleed lg:hidden" aria-hidden="true"></div>
|
|
@endif
|
|
</div>
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('notificationDropdown', (config = {}) => ({
|
|
open: false,
|
|
loading: false,
|
|
notifications: [],
|
|
unreadCount: 0,
|
|
unreadUrl: config.unreadUrl || '/notifications/unread',
|
|
markReadUrl: config.markReadUrl || '/notifications/__ID__/read',
|
|
markAllReadUrl: config.markAllReadUrl || '/notifications/mark-all-read',
|
|
indexUrl: config.indexUrl || '/notifications',
|
|
csrfToken: config.csrfToken || document.querySelector('meta[name="csrf-token"]')?.content || '',
|
|
|
|
init() {
|
|
this.fetchUnread();
|
|
setInterval(() => this.fetchUnread(), 60000);
|
|
},
|
|
|
|
async fetchUnread() {
|
|
try {
|
|
const res = await fetch(this.unreadUrl, {
|
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
|
});
|
|
const data = await res.json();
|
|
this.notifications = data.notifications || [];
|
|
this.unreadCount = data.unread_count || 0;
|
|
} catch (e) {
|
|
console.error('Failed to fetch notifications', e);
|
|
}
|
|
},
|
|
|
|
toggle() {
|
|
this.open = !this.open;
|
|
if (this.open) {
|
|
this.loading = true;
|
|
this.fetchUnread().finally(() => this.loading = false);
|
|
}
|
|
},
|
|
|
|
async markRead(id) {
|
|
const url = this.markReadUrl.replace('__ID__', id);
|
|
try {
|
|
await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': this.csrfToken,
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
});
|
|
this.notifications = this.notifications.filter(n => n.id !== id);
|
|
this.unreadCount = Math.max(0, this.unreadCount - 1);
|
|
} catch (e) {
|
|
console.error('Failed to mark notification as read', e);
|
|
}
|
|
},
|
|
|
|
async markAllRead() {
|
|
try {
|
|
await fetch(this.markAllReadUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': this.csrfToken,
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
});
|
|
this.notifications = [];
|
|
this.unreadCount = 0;
|
|
} catch (e) {
|
|
console.error('Failed to mark all notifications as read', e);
|
|
}
|
|
},
|
|
|
|
getIconBg(icon) {
|
|
const map = {
|
|
website: 'bg-blue-50',
|
|
domain: 'bg-emerald-50',
|
|
hosting: 'bg-violet-50',
|
|
email: 'bg-pink-50',
|
|
billing: 'bg-amber-50',
|
|
lead: 'bg-cyan-50',
|
|
success: 'bg-green-50',
|
|
};
|
|
return map[icon] || 'bg-slate-100';
|
|
},
|
|
|
|
getIconColor(icon) {
|
|
const map = {
|
|
website: 'text-blue-600',
|
|
domain: 'text-emerald-600',
|
|
hosting: 'text-violet-600',
|
|
email: 'text-pink-600',
|
|
billing: 'text-amber-600',
|
|
lead: 'text-cyan-600',
|
|
success: 'text-green-600',
|
|
};
|
|
return map[icon] || 'text-slate-500';
|
|
},
|
|
}));
|
|
|
|
Alpine.data('balanceGate', (config = {}) => ({
|
|
balance: Number(config.balance ?? 0),
|
|
price: Number(config.price ?? 0),
|
|
modalId: config.modalId ?? null,
|
|
topupUrl: config.topupUrl ?? null,
|
|
href: config.href ?? null,
|
|
requiresPayment: Object.prototype.hasOwnProperty.call(config, 'requiresPayment')
|
|
? config.requiresPayment !== false
|
|
: true,
|
|
needsTopupFor(requiresPayment = this.requiresPayment) {
|
|
if (requiresPayment === false) {
|
|
return false;
|
|
}
|
|
|
|
return this.balance <= 0 || this.balance < this.price;
|
|
},
|
|
needsTopup() {
|
|
return this.needsTopupFor(this.requiresPayment);
|
|
},
|
|
openTopup() {
|
|
if (this.topupUrl) {
|
|
window.location.href = this.topupUrl;
|
|
return;
|
|
}
|
|
|
|
if (! this.modalId) {
|
|
return;
|
|
}
|
|
|
|
window.dispatchEvent(new CustomEvent('open-modal', {
|
|
detail: this.modalId,
|
|
bubbles: true,
|
|
}));
|
|
},
|
|
goOrTopup(event, href = null, requiresPayment = null) {
|
|
const paymentRequired = requiresPayment === null
|
|
? this.requiresPayment
|
|
: requiresPayment !== false;
|
|
|
|
if (this.needsTopupFor(paymentRequired)) {
|
|
event?.preventDefault();
|
|
this.openTopup();
|
|
return;
|
|
}
|
|
|
|
const target = href ?? this.href;
|
|
if (target) {
|
|
window.location.href = target;
|
|
}
|
|
},
|
|
submitOrTopup(event, form) {
|
|
if (this.needsTopup()) {
|
|
event?.preventDefault();
|
|
this.openTopup();
|
|
return;
|
|
}
|
|
|
|
if (event?.type === 'submit') {
|
|
return;
|
|
}
|
|
|
|
const targetForm = form || event?.target?.closest?.('form') || this.$refs?.createForm;
|
|
targetForm?.requestSubmit();
|
|
},
|
|
}));
|
|
});
|
|
</script>
|
|
@include('partials.afia')
|
|
@include('partials.sso-keepalive')
|
|
@include('partials.confirm-prompt')
|
|
</body>
|
|
</html>
|