412 lines
14 KiB
PHP
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>
|