feat: add resources and view components
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
<x-app-layout>
|
||||
@push('styles')
|
||||
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css"
|
||||
rel="stylesheet" />
|
||||
<style>
|
||||
/* Shared Styles from Backup & Storage */
|
||||
.status-widget-dark {
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
|
||||
padding: 1.5rem;
|
||||
color: white;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Attached Design Preview Styles */
|
||||
.mockup-container {
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
.browser-mockup-frame {
|
||||
background: #e2e8f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.browser-top-bar {
|
||||
background: #f8fafc;
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.browser-dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.browser-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.browser-url-field {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
padding: 3px 15px;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
flex-grow: 1;
|
||||
max-width: 450px;
|
||||
text-align: center;
|
||||
border: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
/* VISITOR VIEW BACKGROUND (STRICT AS PER IMAGE) */
|
||||
.viewport-preview {
|
||||
height: 700px;
|
||||
background: #f4f4f4;
|
||||
/* Base light gray */
|
||||
background-image: repeating-linear-gradient(45deg, #f0f2f5, #f0f2f5 10px, #ffffff 10px, #ffffff 20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* WHITE CARD (STRICT AS PER IMAGE) */
|
||||
.visitor-card-premium {
|
||||
background: #ffffff;
|
||||
border-radius: 42px;
|
||||
padding: 4rem 3rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
box-shadow: 0 15px 35px -5px rgba(0, 0, 0, 0.04);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.under-maintenance-pill {
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef0f3;
|
||||
border-radius: 100px;
|
||||
padding: 8px 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 2.5rem;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: #1a1c1e;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.pulse-dot-red {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #ef4444;
|
||||
border-radius: 50%;
|
||||
animation: pulse-red 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-red {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* COUNTDOWN SQUARES (STRICT AS PER IMAGE) */
|
||||
.countdown-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.countdown-square {
|
||||
background: #1a1c1e;
|
||||
border-radius: 20px;
|
||||
aspect-ratio: 1/1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.countdown-square:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.countdown-number {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.countdown-label {
|
||||
font-size: 7px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.5;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.custom-switch-premium .form-check-input {
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filepond--root {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filepond--panel-root {
|
||||
background-color: #f8fafc;
|
||||
border: 2px dashed #e2e8f0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
<div class="container-fluid pb-5">
|
||||
{{-- Page Header --}}
|
||||
<div class="d-flex align-items-center justify-content-between mb-4 animate__animated animate__fadeIn">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-1" style="font-family: 'Outfit', sans-serif; letter-spacing: -0.5px;">
|
||||
{{ __('Maintenance Mode') }}
|
||||
</h4>
|
||||
<p class="text-muted small mb-0">
|
||||
{{ __('Take your application offline for scheduled updates and optimization.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="maintenanceConfigForm" action="{{ route('system-config.update') }}" method="POST"
|
||||
enctype="multipart/form-data" autocomplete="off" class="ajax-form" data-reset="false">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row gx-4">
|
||||
{{-- Left: Controls (Col-4) --}}
|
||||
<div class="col-lg-4 animate__animated animate__fadeIn">
|
||||
|
||||
{{-- Status Card (Backup Styling) --}}
|
||||
<div class="status-widget-dark">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="bg-white bg-opacity-10 rounded-circle p-2">
|
||||
<i class="bi bi-broadcast text-info"></i>
|
||||
</div>
|
||||
<span class="small fw-bold opacity-75 text-uppercase tracking-wider">Storage
|
||||
Health</span>
|
||||
</div>
|
||||
@if($is_down)
|
||||
<span class="badge rounded-pill bg-danger text-white fw-bold px-3 py-2"
|
||||
style="font-size: 10px;">Maintenance</span>
|
||||
@else
|
||||
<span class="badge rounded-pill bg-info text-dark fw-bold px-3 py-2"
|
||||
style="font-size: 10px;">Operational</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h2 class="mb-1 fw-bold" style="font-family: 'Outfit', sans-serif;">
|
||||
@if($is_down)
|
||||
System Offline
|
||||
@else
|
||||
Systems Ready
|
||||
@endif
|
||||
</h2>
|
||||
<p class="small opacity-50 mb-0">Public access control center.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="form-check form-switch custom-switch-premium p-0 d-flex align-items-center justify-content-between bg-white bg-opacity-5 p-3 rounded-4 border border-white border-opacity-10">
|
||||
<label class="form-check-label fw-semibold small text-white"
|
||||
for="maintenance_mode_enabled">{{ __('ENABLE MODE') }}</label>
|
||||
<input class="form-check-input ms-0" type="checkbox" role="switch"
|
||||
id="maintenance_mode_enabled" name="maintenance_mode_enabled" value="1"
|
||||
@checked(old('maintenance_mode_enabled', $settings['maintenance_mode_enabled'] ?? false))>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Config Sections --}}
|
||||
<div class="card adminuiux-card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-bold mb-4 d-flex align-items-center gap-2">
|
||||
<i class="bi bi-palette-fill text-primary"></i>
|
||||
{{ __('Visual & Branding') }}
|
||||
</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{{ __('Main Headline') }}</label>
|
||||
<input type="text" class="form-control" name="maintenance_mode_title"
|
||||
value="{{ $settings['maintenance_mode_title'] ?? 'biiproject.com' }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{{ __('Description') }}</label>
|
||||
<textarea class="form-control" name="maintenance_mode_message"
|
||||
rows="3">{{ $settings['maintenance_mode_message'] ?? 'We are currently performing scheduled maintenance. We will be back shortly!' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-semibold mb-2">{{ __('Illustration / Logo') }}</label>
|
||||
<input type="file" id="maintenance_mode_image" name="maintenance_mode_image"
|
||||
accept="image/png,image/jpeg,image/svg+xml">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card adminuiux-card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-bold mb-4 d-flex align-items-center gap-2">
|
||||
<i class="bi bi-clock-fill text-primary"></i>
|
||||
{{ __('Time & Access') }}
|
||||
</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{{ __('Secret Bypass Key') }}</label>
|
||||
<input type="text" class="form-control" name="maintenance_mode_secret"
|
||||
placeholder="e.g. admin-only"
|
||||
value="{{ $settings['maintenance_mode_secret'] ?? '' }}">
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-12 mb-2">
|
||||
<label class="form-label fw-semibold">{{ __('End Time') }}</label>
|
||||
<input type="datetime-local" class="form-control" name="maintenance_mode_end_at"
|
||||
value="{{ !empty($settings['maintenance_mode_end_at']) ? date('Y-m-d\TH:i', strtotime($settings['maintenance_mode_end_at'])) : '' }}">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">{{ __('Retry Interval (Seconds)') }}</label>
|
||||
<input type="number" class="form-control" name="maintenance_mode_retry"
|
||||
value="{{ $settings['maintenance_mode_retry'] ?? 3600 }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card adminuiux-card mb-4 border-warning border-opacity-25 bg-warning bg-opacity-5">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-bold mb-3 d-flex align-items-center gap-2 text-warning-emphasis">
|
||||
<i class="bi bi-megaphone-fill"></i>
|
||||
{{ __('Broadcast Warning') }}
|
||||
</h6>
|
||||
<p class="extra-small text-muted mb-4">
|
||||
{{ __('Alert all active users before shutting down the system. They will receive a real-time notification.') }}
|
||||
</p>
|
||||
|
||||
<div class="input-group">
|
||||
<select class="form-select" id="broadcast_minutes">
|
||||
<option value="1">1 {{ __('Minute') }}</option>
|
||||
<option value="5" selected>5 {{ __('Minutes') }}</option>
|
||||
<option value="10">10 {{ __('Minutes') }}</option>
|
||||
<option value="30">30 {{ __('Minutes') }}</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-warning fw-bold px-3" id="btn-broadcast-warning">
|
||||
<i class="bi bi-send-fill me-1"></i> {{ __('Send Alert') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@can('manage maintenance mode')
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary rounded-pill px-4 shadow-sm">
|
||||
{{ __('Apply Configuration') }}
|
||||
</button>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
{{-- Right: Live Preview (Col-8) --}}
|
||||
<div class="col-lg-8 animate__animated animate__fadeIn">
|
||||
<div class="mockup-container">
|
||||
<div class="browser-mockup-frame">
|
||||
{{-- Toolbar --}}
|
||||
<div class="browser-top-bar">
|
||||
<div class="browser-dots">
|
||||
<div class="browser-dot" style="background: #ff5f56;"></div>
|
||||
<div class="browser-dot" style="background: #ffbd2e;"></div>
|
||||
<div class="browser-dot" style="background: #27c93f;"></div>
|
||||
</div>
|
||||
<div class="browser-url-field">
|
||||
{{ url('/') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Viewport (Design from Image) --}}
|
||||
<div class="viewport-preview">
|
||||
<div class="visitor-card-premium">
|
||||
{{-- Maintenance Pill --}}
|
||||
<div class="under-maintenance-pill">
|
||||
<span class="pulse-dot-red"></span>
|
||||
{{ __('UNDER MAINTENANCE') }}
|
||||
</div>
|
||||
|
||||
{{-- Logo Container --}}
|
||||
<div class="mb-4 mx-auto" id="preview-mnt-image-container"
|
||||
style="max-width: 140px; min-height: 80px; display: flex; align-items: center; justify-content: center;">
|
||||
@php
|
||||
$mnt_img = $settings['maintenance_mode_image'] ?? '';
|
||||
$display_img = null;
|
||||
if (!empty($mnt_img)) {
|
||||
$display_img = str_starts_with($mnt_img, 'assets/') ? asset($mnt_img) : asset('storage/' . $mnt_img);
|
||||
} elseif (!empty($settings['app_logo'])) {
|
||||
$display_img = str_starts_with($settings['app_logo'], 'assets/') ? asset($settings['app_logo']) : asset('storage/' . $settings['app_logo']);
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if($display_img)
|
||||
<img src="{{ $display_img }}" class="img-fluid" id="mnt-preview-img" alt="Logo">
|
||||
@else
|
||||
<i class="bi bi-gear-wide-connected fs-1 text-secondary opacity-25"
|
||||
style="font-size: 3.5rem !important;"></i>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
<h2 class="fw-black mb-3 text-dark" id="preview-mnt-title"
|
||||
style="font-family: 'Outfit', sans-serif;">
|
||||
{{ $settings['maintenance_mode_title'] ?? 'biiproject.com' }}
|
||||
</h2>
|
||||
<p class="text-secondary small mb-5" id="preview-mnt-message"
|
||||
style="line-height: 1.6; opacity: 0.8; font-weight: 500;">
|
||||
{{ $settings['maintenance_mode_message'] ?? 'We are currently performing scheduled maintenance. We will be back shortly!' }}
|
||||
</p>
|
||||
|
||||
{{-- Countdown Grid (Image Style) --}}
|
||||
<div class="countdown-grid" id="preview-mnt-countdown">
|
||||
@foreach(['Days', 'Hours', 'Mins', 'Secs'] as $label)
|
||||
<div class="countdown-square shadow-lg">
|
||||
<div class="countdown-number" id="cd-{{ strtolower($label) }}">00</div>
|
||||
<div class="countdown-label">{{ $label }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/filepond/dist/filepond.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// Initialize FilePond
|
||||
FilePond.registerPlugin(FilePondPluginImagePreview);
|
||||
const pond = FilePond.create(document.querySelector('#maintenance_mode_image'), {
|
||||
name: 'maintenance_mode_image',
|
||||
labelIdle: '<span class="text-muted small">Drop Logo or <span class="text-primary fw-bold">Browse</span></span>',
|
||||
imagePreviewHeight: 120,
|
||||
stylePanelLayout: 'compact',
|
||||
});
|
||||
|
||||
// Form Handling
|
||||
$('#maintenanceConfigForm').on('ajaxForm:beforeSend', function (e, formData) {
|
||||
const files = pond.getFiles();
|
||||
if (files.length > 0) {
|
||||
formData.set('maintenance_mode_image', files[0].file);
|
||||
}
|
||||
});
|
||||
|
||||
$('#maintenanceConfigForm').on('ajaxForm:success', function (e, response) {
|
||||
StandardSwal.fire({ title: 'Success!', text: response.message, icon: 'success', timer: 1500, showConfirmButton: false });
|
||||
|
||||
if (response.settings && response.settings.maintenance_mode_image) {
|
||||
const path = response.settings.maintenance_mode_image;
|
||||
const newUrl = path.startsWith('assets/') ? `/${path}` : `/storage/${path}`;
|
||||
originalImgSrc = `${newUrl}?v=${new Date().getTime()}`;
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty('is_down')) {
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
pond.removeFiles();
|
||||
});
|
||||
|
||||
// Live Preview Sync
|
||||
$('input[name="maintenance_mode_title"]').on('input', function () {
|
||||
$('#preview-mnt-title').text($(this).val() || 'biiproject.com');
|
||||
});
|
||||
$('textarea[name="maintenance_mode_message"]').on('input', function () {
|
||||
$('#preview-mnt-message').text($(this).val() || 'Maintenance in progress...');
|
||||
});
|
||||
|
||||
// Countdown Logic
|
||||
let countdownInterval;
|
||||
function updateCountdown() {
|
||||
const val = $('input[name="maintenance_mode_end_at"]').val();
|
||||
if (!val) {
|
||||
$('#preview-mnt-countdown').addClass('opacity-25');
|
||||
return;
|
||||
}
|
||||
$('#preview-mnt-countdown').removeClass('opacity-25');
|
||||
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
const target = new Date(val).getTime();
|
||||
|
||||
countdownInterval = setInterval(() => {
|
||||
const now = new Date().getTime();
|
||||
const diff = target - now;
|
||||
|
||||
if (diff < 0) {
|
||||
clearInterval(countdownInterval);
|
||||
$('#cd-days, #cd-hours, #cd-mins, #cd-secs').text('00');
|
||||
return;
|
||||
}
|
||||
|
||||
const d = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const h = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const m = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const s = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
|
||||
$('#cd-days').text(d.toString().padStart(2, '0'));
|
||||
$('#cd-hours').text(h.toString().padStart(2, '0'));
|
||||
$('#cd-mins').text(m.toString().padStart(2, '0'));
|
||||
$('#cd-secs').text(s.toString().padStart(2, '0'));
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$('input[name="maintenance_mode_end_at"]').on('change', updateCountdown);
|
||||
updateCountdown();
|
||||
|
||||
// Image Swap
|
||||
let originalImgSrc = $('#mnt-preview-img').attr('src');
|
||||
pond.on('addfile', (error, file) => {
|
||||
if (!error) {
|
||||
const url = URL.createObjectURL(file.file);
|
||||
$('#preview-mnt-image-container').html(`<img src="${url}" class="img-fluid" id="mnt-preview-img" alt="Logo">`);
|
||||
}
|
||||
});
|
||||
pond.on('removefile', () => {
|
||||
if (originalImgSrc) {
|
||||
$('#preview-mnt-image-container').html(`<img src="${originalImgSrc}" class="img-fluid" id="mnt-preview-img" alt="Logo">`);
|
||||
} else {
|
||||
$('#preview-mnt-image-container').html(`<i class="bi bi-gear-wide-connected fs-1 text-secondary opacity-25" style="font-size: 3.5rem !important;"></i>`);
|
||||
}
|
||||
});
|
||||
|
||||
// Broadcast Warning
|
||||
$('#btn-broadcast-warning').on('click', function () {
|
||||
const minutes = $('#broadcast_minutes').val();
|
||||
const $btn = $(this);
|
||||
|
||||
StandardSwal.fire({
|
||||
title: "{{ __('Send Broadcast Alert?') }}",
|
||||
text: "{{ __('All active users will receive a warning about the upcoming maintenance.') }}",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "{{ __('Send Alert') }}"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>');
|
||||
|
||||
$.post("{{ route('maintenance-mode.broadcast') }}", {
|
||||
_token: "{{ csrf_token() }}",
|
||||
minutes: minutes
|
||||
}, function (response) {
|
||||
StandardSwal.fire({ title: "{{ __('Success') }}", text: response.message, icon: 'success' });
|
||||
}).always(() => $btn.prop('disabled', false).html('<i class="bi bi-send-fill me-1"></i> {{ __("Send Alert") }}'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.x-small {
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
Reference in New Issue
Block a user