Files
timebank-cc-public/resources/views/layouts/app.blade.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

412 lines
14 KiB
PHP

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="@themeId">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta content="{{ csrf_token() }}" name="csrf-token">
<meta content="{{config('app.name')}}" name="title">
@unless(timebank_config('seo.allow_indexing_auth'))
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
@endunless
@auth
<meta name="user-id" content="{{ auth()->id() }}">
<meta name="user-guard" content="{{ config('auth.defaults.guard') }}">
@endauth
@php
// Determine page title: explicit title > header content > fallback
$pageTitle = null;
// Check if header slot is set and extract text from it
if (isset($header) && $header) {
$headerContent = (string) $header;
$pageTitle = trim(strip_tags($headerContent));
}
// Fall back to StringHelper if no valid header content
if (empty($pageTitle)) {
$pageTitle = \App\Helpers\StringHelper::getPageTitle();
}
@endphp
<title>@yield('title', $pageTitle) - {{ config('app.name') }}</title>
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('favicon-16x16.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('apple-touch-icon.png') }}">
<!-- Scripts head -->
<script src="{{ route('lang.js') }}"></script>
<!-- Fonts -->
{{-- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap"> --}}
<!-- Styles -->
<link href="{{ asset('css/tagify.css') }}" rel="stylesheet">
<!-- Dynamic Theme CSS Custom Properties -->
<style>
:root {
{!! theme_css_vars() !!}
}
</style>
@vite(['resources/css/app.css', 'resources/css/fonts.css', 'resources/sass/custom_timebank.css'])
<link href="{{ asset('css/custom_tagify.css') }}" rel="stylesheet">
@livewireStyles
@wirechatStyles
<!-- Flatpickr Styles -->
<x-flatpickr::style />
<style>
/* Theme-aware typography */
body {
font-family: var(--font-family-body, 'Poppins', sans-serif) !important;
}
/* Theme-aware heading styles */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-family-heading, 'Oswald', sans-serif) !important;
text-transform: var(--heading-transform, uppercase) !important;
}
/* Apply theme-specific CSS custom properties */
:root {
@themeCssVars
}
</style>
<!-- Quill editor (loaded locally via Vite) -->
@vite('resources/js/quill.js')
<style>
.ql-editor {
height: 500px;
}
</style>
<!-- Scripts (wireui moved to body to fix Firefox Alpine init order) -->
{{-- TODO: Move styles below to separate css file, that can be compiled --}}
<style>
input[type=file]::file-selector-button {
border-style: none;
padding: 0;
color: #fff !important;
padding-left: 1.0rem;
padding-top: 0.25rem;
padding-right: 1.0rem;
padding-bottom: 0.25rem;
border-radius: 0.25rem;
-webkit-appearance: button;
font-weight: 700;
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
text-transform: none;
}
input[type=file]::file-selector-button:hover {
cursor: pointer;
--tw-text-opacity: 1;
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
}
progress {
width: 100%;
height: 20px;
border: none;
background-color: #f1f1f1;
}
progress::-webkit-progress-bar {
background-color: #f1f1f1;
}
progress::-webkit-progress-value {
background-color: #9ae6b4;
}
progress::-moz-progress-bar {
background-color: #9ae6b4;
}
</style>
</head>
<body class="font-sans antialiased flex flex-col min-h-screen">
<x-jetstream.banner />
<x-jetstream.toaster />
<div class="flex-grow md:bg-theme-surface">
@livewire('navigation-menu')
<x-notifications position="bottom-end" />
<header class="bg-theme-brand text-xl font-semibold text-theme-background shadow sm:mt-16">
<!-- System Anounnucement -->
@livewire('system-announcement', ['type' => 'SiteContents\SystemAnnouncement' ?? null, 'limit' => 1])
<!-- Maintenance Banner -->
@livewire('admin.maintenance-banner')
<!-- Header --->
@if (isset($header))
<div class="max-w-7xl mx-auto py-2 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
@endif
</header>
<!-- Page Content -->
<main>
{{ $slot }}
</main>
</div>
<!-- Footer -->
<div class="mt-auto w-full">
<x-footer />
</div>
<!-- Forced Logout Modal -->
@auth
@livewire('forced-logout-modal')
@livewire('account-info-modal')
@endauth
<!-- Scripts body-->
<!-- Be careful with changing the loading order! -->
<wireui:scripts />
@livewireScripts
@vite('resources/js/app.js')
@wirechatAssets
<!-- Session Expiration Detection -->
<script>
// Detect session expiration and auto-reload
document.addEventListener('DOMContentLoaded', function() {
let sessionExpired = false;
let isLoggingOut = false;
// Mark when logout is initiated
document.addEventListener('click', function(e) {
const logoutButton = e.target.closest('form[action*="logout"]') ||
e.target.closest('button[wire\\:click*="logout"]') ||
e.target.closest('a[href*="logout"]');
if (logoutButton) {
isLoggingOut = true;
}
});
// Detect Livewire 401/419 errors (session expired)
window.addEventListener('livewire:request', ({ detail }) => {
detail.fail(({ status, content, preventDefault }) => {
if (status === 401 || status === 419) {
preventDefault(); // Always prevent default to avoid "Page expired" dialog
if (!sessionExpired) {
sessionExpired = true;
// Small delay to let other pending requests complete
setTimeout(() => {
// If logging out, redirect to home instead of reloading
if (isLoggingOut) {
window.location.href = '{{ LaravelLocalization::localizeUrl('/') }}';
} else {
window.location.reload();
}
}, 100);
}
}
});
});
// Detect Echo connection errors (session expired)
if (window.Echo && window.Echo.connector && window.Echo.connector.pusher) {
window.Echo.connector.pusher.connection.bind('error', function(err) {
// Check if it's an authentication error
if (err && (err.type === 'AuthError' || err.error?.data?.code === 4009)) {
if (!sessionExpired) {
sessionExpired = true;
// Delay reload slightly to avoid rapid reloads
setTimeout(() => window.location.reload(), 1000);
}
}
});
}
// Catch "Handler does not exist" errors globally
window.addEventListener('error', function(event) {
const message = event.message || '';
if (message.includes('Handler for event') && message.includes('does not exist')) {
// This means session expired and component is stale
if (!sessionExpired) {
sessionExpired = true;
window.location.reload();
}
event.preventDefault();
return false;
}
}, true);
// Suppress the specific Echo error message in console
const originalConsoleError = console.error;
console.error = function(...args) {
const message = args[0]?.toString() || '';
// Don't log Echo handler errors if we're already reloading
if (sessionExpired && message.includes('Handler for event')) {
return;
}
// Suppress handler errors and trigger reload
if (message.includes('Handler for event') && message.includes('does not exist')) {
if (!sessionExpired) {
sessionExpired = true;
setTimeout(() => window.location.reload(), 500);
}
return;
}
originalConsoleError.apply(console, args);
};
});
</script>
<!-- Flatpickr Scripts -->
<x-flatpickr::script />
<!-- Flatpickr Error Handling Override -->
<script>
// Wrap the original Flatpickr initialization with error handling
if (window.LaravelFlatpickr) {
const originalInit = window.LaravelFlatpickr.initializeFlatpickr;
window.LaravelFlatpickr.initializeFlatpickr = function(e) {
try {
// Check if element and required methods exist
if (!e || !e.getAttribute) return;
const targetId = e.getAttribute("data-selector-id");
if (!targetId) return;
const target = document.getElementById(targetId);
if (!target) return;
// Call original initialization
originalInit.call(this, e);
} catch (error) {
// Silently fail to prevent console spam
// console.warn('Flatpickr initialization skipped:', error);
}
};
}
</script>
<!-- Suppress non-critical Alpine.js errors during Livewire morphing -->
<script>
// Suppress WireUI select component errors during Livewire morphing
// These errors occur when Alpine tries to evaluate expressions before the component data is initialized
// They don't affect functionality as the components initialize correctly after morphing completes
// Store original console methods
const originalError = console.error;
const originalWarn = console.warn;
// List of error patterns to suppress
const suppressedPatterns = [
/positionable is not defined/,
/getSelectedValue is not defined/,
/isEmpty is not defined/,
/getPlaceholder is not defined/,
/config is not defined/,
/getSelectedDisplayText is not defined/,
/asyncData is not defined/,
/displayOptions is not defined/,
/isNotEmpty is not defined/,
/selectedOptions is not defined/,
/wireui_select/,
/Alpine Expression Error/,
/ReferenceError.*is not defined/
];
function shouldSuppressMessage(args) {
const message = args.map(arg => {
if (typeof arg === 'string') return arg;
if (arg instanceof Error) return arg.message;
return String(arg);
}).join(' ');
return suppressedPatterns.some(pattern => pattern.test(message));
}
// Override console.error
console.error = function(...args) {
if (!shouldSuppressMessage(args)) {
originalError.apply(console, args);
}
};
// Override console.warn (some frameworks log errors as warnings)
console.warn = function(...args) {
if (!shouldSuppressMessage(args)) {
originalWarn.apply(console, args);
}
};
// Catch uncaught errors in promises and event handlers
window.addEventListener('error', function(event) {
if (shouldSuppressMessage([event.message || event.error?.message || ''])) {
event.preventDefault();
event.stopPropagation();
return false;
}
}, true);
window.addEventListener('unhandledrejection', function(event) {
if (shouldSuppressMessage([event.reason?.message || event.reason || ''])) {
event.preventDefault();
return false;
}
}, true);
</script>
@stack('scripts')
@yield('scripts_body')
@yield('js')
@stack('modals')
<!-- Listen for forced logout event -->
@auth
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.Echo) {
window.Echo.private('user.logout.{{ auth()->id() }}')
.listen('.forced-logout', (e) => {
// Get current locale from HTML lang attribute
const locale = document.documentElement.lang || 'en';
// Translate message in user's locale using window.i18n
const messageKey = e.message_key || 'For security and maintenance, a system administrator has logged you out of your account. Sorry for this inconvenience and thanks for your patience.';
let message = messageKey;
if (window.i18n && window.i18n[locale] && window.i18n[locale][messageKey]) {
message = window.i18n[locale][messageKey];
}
// Show alert and immediately redirect to login page
// This avoids CSRF errors from deleted sessions
alert(message);
// Redirect to login page with a maintenance mode flag
window.location.href = '{{ route("login") }}?logged_out=maintenance';
});
}
});
</script>
@endauth
</body>
</html>