Files
isaaccladandCursor 2499244049
Deploy Ladill Transfer / deploy (push) Has been cancelled
Fix corrupted profileMenuItems in layouts after menu propagation.
Remove orphaned array entries that caused Blade parse errors and 500 responses on authenticated pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 10:04:58 +00:00

261 lines
9.8 KiB
PHP

@php
$mobileFullScreenPage = request()->routeIs('account.settings')
|| request()->routeIs('transfer.transfers.create')
|| request()->routeIs('transfer.transfers.show');
$qrMobilePage = request()->routeIs('transfer.transfers.create') || request()->routeIs('transfer.transfers.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('transfer.transfers.create') || request()->routeIs('transfer.transfers.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('transfer.dashboard'),
'homeActive' => request()->routeIs('transfer.dashboard'),
'searchUrl' => route('transfer.search'),
'searchActive' => request()->routeIs('transfer.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),
'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>