Initial commit

This commit is contained in:
root
2026-05-09 17:28:23 +02:00
commit 9d73f82529
5575 changed files with 281989 additions and 0 deletions
+365
View File
@@ -0,0 +1,365 @@
@extends('layouts.app')
@section('title', __('radio.setup_page_title') . ' - ' . config('app.name'))
@push('styles')
<style>
.setup-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.setup-card {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.setup-header {
text-align: center;
margin-bottom: 3rem;
}
.setup-title {
font-size: 2.5rem;
font-weight: 800;
color: #1a202c;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.setup-subtitle {
font-size: 1.125rem;
color: #4a5568;
max-width: 600px;
margin: 0 auto;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.feature-card {
background: #f7fafc;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #eeb425;
}
.feature-icon {
width: 48px;
height: 48px;
background: #eeb425;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.feature-title {
font-size: 1.25rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 0.5rem;
}
.feature-description {
color: #4a5568;
line-height: 1.6;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.btn-primary {
background: linear-gradient(135deg, #eeb425, #d4a52a);
color: #1a202c;
padding: 1rem 2rem;
border: none;
border-radius: 8px;
font-weight: 700;
font-size: 1.125rem;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 4px 12px rgba(238, 180, 37, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(238, 180, 37, 0.4);
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
padding: 1rem 2rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-secondary:hover {
background: #cbd5e0;
transform: translateY(-1px);
}
.status-check {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #f0fff4;
border-radius: 8px;
margin-bottom: 2rem;
}
.status-check.missing {
background: #fff5f5;
}
.status-icon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
}
.status-icon.configured {
background: #38a169;
}
.status-icon.missing {
background: #e53e3e;
}
.settings-preview {
background: #edf2f7;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.setting-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: white;
border-radius: 4px;
font-size: 0.875rem;
}
.setting-value {
font-weight: 600;
color: #eeb425;
}
@media (max-width: 768px) {
.setup-container {
padding: 1rem;
}
.setup-card {
padding: 1.5rem;
}
.setup-title {
font-size: 2rem;
}
.feature-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
</style>
@endpush
@section('content')
<div class="setup-container">
<!-- Header -->
<div class="setup-header">
<h1 class="setup-title">
<span>🎵</span>
{{ __('radio.setup_page_title') }}
<span>🚀</span>
</h1>
<p class="setup-subtitle">
{{ __('radio.setup_page_subtitle') }}
</p>
</div>
@if(session('success'))
<div class="status-check configured">
<div class="status-icon configured"></div>
<div>
<strong>{{ __('radio.setup_complete') }}</strong><br>
{{ session('success') }}
</div>
</div>
@endif
<!-- Features Overview -->
<div class="setup-card">
<h2 class="text-2xl font-bold mb-6 text-center">{{ __('radio.what_gets_configured') }}</h2>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">📻</div>
<h3 class="feature-title">{{ __('radio.radio_stream') }}</h3>
<p class="feature-description">
{{ __('radio.radio_stream_desc') }}
</p>
</div>
<div class="feature-card">
<div class="feature-icon">🏆</div>
<h3 class="feature-title">{{ __('radio.points_system') }}</h3>
<p class="feature-description">
{{ __('radio.points_system_desc') }}
</p>
</div>
<div class="feature-card">
<div class="feature-icon">💬</div>
<h3 class="feature-title">{{ __('radio.community_features') }}</h3>
<p class="feature-description">
{{ __('radio.community_features_desc') }}
</p>
</div>
<div class="feature-card">
<div class="feature-icon">👨‍🎤</div>
<h3 class="feature-title">{{ __('radio.dj_management') }}</h3>
<p class="feature-description">
{{ __('radio.dj_management_desc') }}
</p>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<h3 class="feature-title">{{ __('radio.monitoring') }}</h3>
<p class="feature-description">
{{ __('radio.monitoring_desc') }}
</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎨</div>
<h3 class="feature-title">{{ __('radio.display_options') }}</h3>
<p class="feature-description">
{{ __('radio.display_options_desc') }}
</p>
</div>
</div>
</div>
<!-- Settings Preview -->
<div class="setup-card">
<h2 class="text-2xl font-bold mb-6 text-center">{{ __('radio.default_settings') }}</h2>
<div class="settings-preview">
<div class="settings-grid">
<div class="setting-item">
<span>📻</span>
<span>{{ __('radio.radio_label') }}</span>
<span class="setting-value">{{ __('radio.enabled') }}</span>
</div>
<div class="setting-item">
<span>🏆</span>
<span>{{ __('radio.points_label') }}</span>
<span class="setting-value">2{{ __('radio.per_min') }}</span>
</div>
<div class="setting-item">
<span>📊</span>
<span>{{ __('radio.daily_limit') }}</span>
<span class="setting-value">100</span>
</div>
<div class="setting-item">
<span>💬</span>
<span>{{ __('radio.shouts_label') }}</span>
<span class="setting-value">{{ __('radio.on') }}</span>
</div>
<div class="setting-item">
<span>📻</span>
<span>{{ __('radio.widget') }}</span>
<span class="setting-value">{{ __('radio.global') }}</span>
</div>
<div class="setting-item">
<span>👨‍🎤</span>
<span>{{ __('radio.dj_apps') }}</span>
<span class="setting-value">{{ __('radio.open') }}</span>
</div>
<div class="setting-item">
<span>📊</span>
<span>{{ __('radio.monitoring_label') }}</span>
<span class="setting-value">{{ __('radio.on') }}</span>
</div>
<div class="setting-item">
<span>🎯</span>
<span>{{ __('radio.contests_label') }}</span>
<span class="setting-value">{{ __('radio.on') }}</span>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<form action="{{ route('admin.radio.setup.do') }}" method="POST">
@csrf
<button type="submit" class="btn-primary">
<span>🚀</span>
{{ __('radio.install_radio_system') }}
</button>
</form>
<form action="{{ route('admin.radio.setup.reset') }}" method="POST">
@csrf
<button type="submit" class="btn-secondary" onclick="return confirm('{{ __('radio.reset_confirm') }}')">
<span>🔄</span>
{{ __('radio.reset_settings') }}
</button>
</form>
<a href="/housekeeping/radio-settings" class="btn-secondary">
<span>⚙️</span>
{{ __('radio.go_to_radio_settings') }}
</a>
</div>
</div>
@endsection
+64
View File
@@ -0,0 +1,64 @@
@extends('layouts.app')
@section('title', __('flash_client'))
@section('content')
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-8">
<h1 class="text-3xl font-bold text-center mb-6">{{ __('flash_client') }}</h1>
<div class="text-center mb-8">
<p class="text-gray-600 mb-4">
{{ __('welcome_flash') }}
</p>
<p class="text-sm text-gray-500">
{{ __('sso_ticket') }}
</p>
</div>
<div class="bg-gray-50 rounded-lg p-6 mb-4">
<h3 class="font-semibold mb-2">{{ __('download_options') }}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h4 class="font-medium mb-2">{{ __('windows') }}</h4>
<p class="text-sm text-gray-600 mb-2">{{ __('recommended_windows') }}</p>
<a href="#" class="block w-full text-center bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">
{{ __('download_exe') }}
</a>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h4 class="font-medium mb-2">{{ __('macos') }}</h4>
<p class="text-sm text-gray-600 mb-2">{{ __('for_macos') }}</p>
<a href="#" class="block w-full text-center bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">
{{ __('download_dmg') }}
</a>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h4 class="font-medium mb-2">{{ __('linux') }}</h4>
<p class="text-sm text-gray-600 mb-2">{{ __('for_linux') }}</p>
<a href="#" class="block w-full text-center bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">
{{ __('download_appimage') }}
</a>
</div>
</div>
</div>
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
<h4 class="font-semibold text-green-800 mb-2">{{ __('sso_authentication') }}</h4>
<p class="text-sm text-green-700">
{{ __('sso_description') }}
</p>
</div>
<div class="flex justify-center">
<a href="{{ route('welcome') }}" class="bg-gray-600 text-white px-6 py-2 rounded hover:bg-gray-700 transition">
{{ __('back_to_home') }}
</a>
</div>
</div>
</div>
</div>
@endsection
+64
View File
@@ -0,0 +1,64 @@
@extends('layouts.app')
@section('title', __('nitro_client'))
@section('content')
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-8">
<h1 class="text-3xl font-bold text-center mb-6">{{ __('nitro_client') }}</h1>
<div class="text-center mb-8">
<p class="text-gray-600 mb-4">
{{ __('welcome_nitro') }}
</p>
<p class="text-sm text-gray-500">
{{ __('sso_ticket') }}
</p>
</div>
<div class="bg-gray-50 rounded-lg p-6 mb-4">
<h3 class="font-semibold mb-2">{{ __('download_options') }}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h4 class="font-medium mb-2">{{ __('windows') }}</h4>
<p class="text-sm text-gray-600 mb-2">{{ __('recommended_windows') }}</p>
<a href="#" class="block w-full text-center bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">
{{ __('download_exe') }}
</a>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h4 class="font-medium mb-2">{{ __('macos') }}</h4>
<p class="text-sm text-gray-600 mb-2">{{ __('for_macos') }}</p>
<a href="#" class="block w-full text-center bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">
{{ __('download_dmg') }}
</a>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h4 class="font-medium mb-2">{{ __('linux') }}</h4>
<p class="text-sm text-gray-600 mb-2">{{ __('for_linux') }}</p>
<a href="#" class="block w-full text-center bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">
{{ __('download_appimage') }}
</a>
</div>
</div>
</div>
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
<h4 class="font-semibold text-green-800 mb-2">{{ __('sso_authentication') }}</h4>
<p class="text-sm text-green-700">
{{ __('sso_description') }}
</p>
</div>
<div class="flex justify-center">
<a href="{{ route('welcome') }}" class="bg-gray-600 text-white px-6 py-2 rounded hover:bg-gray-700 transition">
{{ __('back_to_home') }}
</a>
</div>
</div>
</div>
</div>
@endsection
+82
View File
@@ -0,0 +1,82 @@
@extends('layouts.app')
@section('title', __('radio.become_dj') . ' - ' . config('app.name'))
@section('content')
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-white">
<h2 class="text-2xl font-bold mb-4">{{ __('radio.become_dj') }}</h2>
@if(session('success'))
<div class="bg-green-500 text-white p-4 rounded mb-4">
{{ session('success') }}
</div>
@endif
@if($errors->any())
<div class="bg-red-500 text-white p-4 rounded mb-4">
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if(isset($hasPendingApplication) && $hasPendingApplication)
<div class="bg-blue-500 text-white p-4 rounded">
{{ __('radio.application_pending') }}
</div>
@else
<form action="{{ route('radio.apply.store') }}" method="POST">
@csrf
<div class="grid grid-cols-1 gap-6">
<div>
<label for="real_name" class="block text-sm font-medium text-gray-300">{{ __('radio.real_name') }}</label>
<input type="text" name="real_name" id="real_name" required class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white">
</div>
<div>
<label for="age" class="block text-sm font-medium text-gray-300">{{ __('radio.age') }}</label>
<input type="number" name="age" id="age" required min="13" max="99" class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white">
</div>
<div>
<label for="discord_username" class="block text-sm font-medium text-gray-300">{{ __('radio.discord') }}</label>
<input type="text" name="discord_username" id="discord_username" class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white">
</div>
<div>
<label for="experience" class="block text-sm font-medium text-gray-300">{{ __('radio.experience') }}</label>
<textarea name="experience" id="experience" rows="3" class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white"></textarea>
</div>
<div>
<label for="music_style" class="block text-sm font-medium text-gray-300">{{ __('radio.music_style') }}</label>
<textarea name="music_style" id="music_style" rows="2" class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white"></textarea>
</div>
<div>
<label for="availability" class="block text-sm font-medium text-gray-300">{{ __('radio.availability') }}</label>
<textarea name="availability" id="availability" required rows="2" class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white"></textarea>
</div>
<div>
<label for="motivation" class="block text-sm font-medium text-gray-300">{{ __('radio.motivation') }}</label>
<textarea name="motivation" id="motivation" required rows="4" class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white"></textarea>
</div>
<div>
<button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
{{ __('radio.send_application') }}
</button>
</div>
</div>
</form>
@endif
</div>
</div>
</div>
@endsection
+45
View File
@@ -0,0 +1,45 @@
@extends('layouts.app')
@section('title', __('radio.contests') . ' - ' . config('app.name'))
@section('content')
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-white">
<h2 class="text-2xl font-bold mb-6">{{ __('radio.active_contests') }}</h2>
@if(session('error'))
<div class="bg-red-500 text-white p-4 rounded mb-4">
{{ session('error') }}
</div>
@endif
@if($contests->isEmpty())
<div class="bg-gray-700 p-6 rounded-lg text-center">
<p class="text-gray-300">{{ __('radio.no_contests') }}</p>
</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($contests as $contest)
<div class="bg-gray-700 rounded-lg overflow-hidden shadow-lg">
<div class="p-6">
<h3 class="text-xl font-bold mb-2">{{ $contest->title }}</h3>
<p class="text-gray-300 mb-4 line-clamp-3">{{ $contest->description }}</p>
<div class="flex justify-between items-center text-sm text-gray-400 mb-4">
<span>{{ __('radio.ends_in', ['time' => $contest->ends_at->diffForHumans()]) }}</span>
<span>{{ $contest->entryCount() }} {{ __('radio.participants') }}</span>
</div>
<a href="{{ route('radio.contests.show', $contest) }}" class="block w-full bg-indigo-600 hover:bg-indigo-700 text-center text-white font-bold py-2 px-4 rounded transition duration-200">
{{ __('radio.view_participate') }}
</a>
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
@endsection
+37
View File
@@ -0,0 +1,37 @@
@extends('layouts.app')
@section('title', $contest->title . ' - ' . __('radio.contests') . ' - ' . config('app.name'))
@section('content')
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-white">
<div class="mb-6">
<a href="{{ route('radio.contests.index') }}" class="text-indigo-400 hover:text-indigo-300 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
{{ __('radio.back_overview') }}
</a>
</div>
<h1 class="text-3xl font-bold mb-4">{{ $contest->title }}</h1>
<div class="prose prose-invert max-w-none mb-8">
{!! nl2br(e($contest->description)) !!}
</div>
<div class="bg-gray-700 p-6 rounded-lg mb-8">
<h3 class="text-xl font-semibold mb-4">{{ __('radio.information') }}</h3>
<ul class="space-y-2 text-gray-300">
<li><strong>{{ __('radio.start_date') }}:</strong> {{ $contest->starts_at->format('d-m-Y H:i') }}</li>
<li><strong>{{ __('radio.end_date') }}:</strong> {{ $contest->ends_at->format('d-m-Y H:i') }}</li>
<li><strong>{{ __('radio.participants') }}:</strong> {{ $contest->entryCount() }}</li>
</ul>
</div>
<div class="bg-gray-700 p-6 rounded-lg">
<p class="text-center text-gray-300">{{ __('radio.participate_button') }} form placeholder.</p>
</div>
</div>
</div>
</div>
@endsection
+45
View File
@@ -0,0 +1,45 @@
@extends('layouts.app')
@section('title', __('radio.giveaways') . ' - ' . config('app.name'))
@section('content')
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-white">
<h2 class="text-2xl font-bold mb-6">{{ __('radio.active_giveaways') }}</h2>
@if(session('error'))
<div class="bg-red-500 text-white p-4 rounded mb-4">
{{ session('error') }}
</div>
@endif
@if($giveaways->isEmpty())
<div class="bg-gray-700 p-6 rounded-lg text-center">
<p class="text-gray-300">{{ __('radio.no_giveaways') }}</p>
</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($giveaways as $giveaway)
<div class="bg-gray-700 rounded-lg overflow-hidden shadow-lg">
<div class="p-6">
<h3 class="text-xl font-bold mb-2">{{ $giveaway->title }}</h3>
<p class="text-gray-300 mb-4 line-clamp-3">{{ $giveaway->description }}</p>
<div class="flex justify-between items-center text-sm text-gray-400 mb-4">
<span>{{ __('radio.ends_in', ['time' => $giveaway->ends_at->diffForHumans()]) }}</span>
<span>{{ $giveaway->participantCount() }} {{ __('radio.participants') }}</span>
</div>
<a href="{{ route('radio.giveaways.show', $giveaway) }}" class="block w-full bg-indigo-600 hover:bg-indigo-700 text-center text-white font-bold py-2 px-4 rounded transition duration-200">
{{ __('radio.view_participate') }}
</a>
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
@endsection
+38
View File
@@ -0,0 +1,38 @@
@extends('layouts.app')
@section('title', $giveaway->title . ' - ' . __('radio.giveaways') . ' - ' . config('app.name'))
@section('content')
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-white">
<div class="mb-6">
<a href="{{ route('radio.giveaways.index') }}" class="text-indigo-400 hover:text-indigo-300 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
{{ __('radio.back_overview') }}
</a>
</div>
<h1 class="text-3xl font-bold mb-4">{{ $giveaway->title }}</h1>
<div class="prose prose-invert max-w-none mb-8">
{!! nl2br(e($giveaway->description)) !!}
</div>
<div class="bg-gray-700 p-6 rounded-lg mb-8">
<h3 class="text-xl font-semibold mb-4">{{ __('radio.information') }}</h3>
<ul class="space-y-2 text-gray-300">
<li><strong>{{ __('radio.start_date') }}:</strong> {{ $giveaway->starts_at->format('d-m-Y H:i') }}</li>
<li><strong>{{ __('radio.end_date') }}:</strong> {{ $giveaway->ends_at->format('d-m-Y H:i') }}</li>
<li><strong>{{ __('radio.participants') }}:</strong> {{ $giveaway->participantCount() }}</li>
<li><strong>{{ __('radio.prize_value') }}:</strong> {{ $giveaway->prize_value }} Credits</li>
</ul>
</div>
<div class="bg-gray-700 p-6 rounded-lg">
<p class="text-center text-gray-300">{{ __('radio.participate_button') }} button placeholder.</p>
</div>
</div>
</div>
</div>
@endsection
+264
View File
@@ -0,0 +1,264 @@
@extends('layouts.app')
@section('title', __('radio.music') . ' - ' . config('app.name'))
@push('styles')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/inter@4.x/400-700.css">
<style>
:root {
--radio-primary: {{ $primaryColor ?? '#eeb425' }};
--radio-secondary: {{ $secondaryColor ?? '#1a1a2e' }};
--radio-text: {{ $textColor ?? '#ffffff' }};
--radio-accent: {{ $accentColor ?? '#ff6b6b' }};
}
.radio-container {
background: linear-gradient(135deg, var(--radio-secondary) 0%, #0f0f1a 100%);
min-height: 100vh;
}
.radio-player {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.now-playing-card {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.animate-pulse-slow {
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.live-badge {
background: linear-gradient(90deg, #ef4444, #f97316);
animation: livePulse 2s infinite;
}
@keyframes livePulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
50% { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
}
</style>
@endpush
@section('content')
<div class="radio-container text-white">
@if(!$enabled)
<!-- Radio Disabled State -->
<div class="min-h-screen flex items-center justify-center p-6">
<div class="text-center max-w-md">
<div class="mb-6">
<svg class="w-24 h-24 mx-auto text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
<h1 class="text-3xl font-bold mb-4">{{ __('radio.offline') }}</h1>
<p class="text-gray-400 mb-6">{{ __('radio.offline_message') }}</p>
@if($offlineMessage)
<div class="bg-gray-800 rounded-lg p-4">
<p class="text-gray-300">{{ $offlineMessage }}</p>
</div>
@endif
</div>
</div>
@else
<!-- Radio Player Section -->
<div class="max-w-6xl mx-auto px-4 py-8">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold flex items-center gap-3">
<span class="live-badge inline-flex items-center px-3 py-1 rounded-full text-xs font-bold">
<span class="w-2 h-2 bg-white rounded-full mr-2"></span>
{{ __('radio.live') }}
</span>
{{ __('radio.music') }}
</h1>
<p class="text-gray-400 mt-1">{{ __('radio.music_desc') }}</p>
</div>
<!-- Stats -->
<div class="flex gap-6">
@if($showListeners)
<div class="stat-card rounded-lg px-4 py-2 flex items-center gap-2">
<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span id="listenerCount" class="font-semibold">--</span>
<span class="text-gray-400 text-sm">{{ __('radio.listeners') }}</span>
</div>
@endif
<a href="{{ route('radio.leaderboard') }}" class="stat-card rounded-lg px-4 py-2 flex items-center gap-2 hover:bg-white/10 transition">
<svg class="w-5 h-5 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
<span class="font-semibold">{{ __('radio.top_100') }}</span>
</a>
</div>
</div>
<!-- Main Player -->
<div class="radio-player rounded-2xl p-6 mb-8">
<div class="grid md:grid-cols-3 gap-6 items-center">
<!-- Album Art / DJ Avatar -->
<div class="flex justify-center">
<div id="djAvatar" class="w-32 h-32 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center text-4xl">
@if($currentDJ && $currentDJ->user)
<img src="{{ $currentDJ->user->avatar_url ?? asset('images/default-avatar.png') }}" alt="{{ $currentDJ->user->username }}" class="w-full h-full rounded-full object-cover">
@else
🎵
@endif
</div>
</div>
<!-- Track Info -->
<div class="text-center">
<div id="nowPlayingInfo">
<p class="text-gray-400 text-sm mb-1">{{ __('radio.now_playing') }}</p>
<h2 id="trackTitle" class="text-xl font-bold mb-2">
@if($nowPlaying && $nowPlaying['title'])
{{ $nowPlaying['title'] }}
@else
--
@endif
</h2>
@if($nowPlaying && $nowPlaying['artist'])
<p id="trackArtist" class="text-gray-400">{{ $nowPlaying['artist'] }}</p>
@endif
</div>
<!-- DJ Info -->
@if($showCurrentDJ && $currentDJ && $currentDJ->user)
<div class="mt-4 pt-4 border-t border-white/10">
<p class="text-gray-400 text-sm mb-1">{{ __('radio.host') }}</p>
<p class="font-semibold text-amber-400">{{ $currentDJ->user->username }}</p>
</div>
@endif
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const player = document.getElementById('radioPlayer');
const playBtn = document.getElementById('playPauseBtn');
const playIcon = document.getElementById('playIcon');
const pauseIcon = document.getElementById('pauseIcon');
const volumeSlider = document.getElementById('volumeSlider');
const listenerCount = document.getElementById('listenerCount');
// Play/Pause
playBtn.addEventListener('click', function() {
if (player.paused) {
player.play().catch(function(e) {
console.log('Autoplay blocked:', e);
});
playIcon.classList.add('hidden');
pauseIcon.classList.remove('hidden');
} else {
player.pause();
playIcon.classList.remove('hidden');
pauseIcon.classList.add('hidden');
}
});
// Volume
volumeSlider.addEventListener('input', function() {
player.volume = this.value / 100;
});
// Initial volume
player.volume = volumeSlider.value / 100;
// Update listeners count
function updateListeners() {
fetch('/api/radio/listeners')
.then(r => r.json())
.then(data => {
if (listenerCount) {
listenerCount.textContent = data.count.toLocaleString();
}
})
.catch(() => {
if (listenerCount) listenerCount.textContent = '--';
});
}
// Update now playing
function updateNowPlaying() {
fetch('/api/radio/now-playing')
.then(r => r.json())
.then(data => {
const titleEl = document.getElementById('trackTitle');
const artistEl = document.getElementById('trackArtist');
if (titleEl && data.title) titleEl.textContent = data.title;
if (artistEl && data.artist) artistEl.textContent = data.artist;
})
.catch(() => {});
}
// Auto-update listeners every 30 seconds
updateListeners();
setInterval(updateListeners, 30000);
// Auto-update now playing every 15 seconds
updateNowPlaying();
setInterval(updateNowPlaying, 15000);
// Load user points if logged in
if (document.body.classList.contains('authenticated')) {
loadUserPoints();
setInterval(loadUserPoints, 60000); // Update every minute
}
});
function loadUserPoints() {
fetch('/api/radio/points/user')
.then(response => {
if (!response.ok) {
throw new Error('Not authenticated');
}
return response.json();
})
.then(data => {
const pointsCount = document.getElementById('userPointsCount');
const userRank = document.getElementById('userRank');
const pointsHistory = document.getElementById('pointsHistory');
if (pointsCount) pointsCount.textContent = data.points || 0;
if (userRank) userRank.textContent = data.rank || '--';
if (pointsHistory && data.history && data.history.length > 0) {
pointsHistory.innerHTML = data.history.map(item => `
<div class="flex justify-between items-center py-1 border-b border-gray-700 text-xs">
<span class="text-gray-300">${item.reason}</span>
<span class="${item.points > 0 ? 'text-green-400' : 'text-red-400'}">
${item.points > 0 ? '+' : ''}${item.points}
</span>
</div>
`).join('');
} else if (pointsHistory) {
pointsHistory.innerHTML = '<p class="text-xs text-gray-500">{{ __("radio.no_history") }}</p>';
}
})
.catch(error => {
console.log('User points not available:', error.message);
// Silently fail if not authenticated or other error
});
}
</script>
@endpush
+318
View File
@@ -0,0 +1,318 @@
<x-app-layout>
@push('title', 'Radio Leaderboard')
<div class="col-span-12 lg:col-span-9 lg:w-[96%]">
<div class="flex flex-col gap-y-4">
<x-content.content-card icon="trophy" classes="border dark:border-gray-900">
<x-slot:title>
Radio Leaderboard
</x-slot:title>
<x-slot:under-title>
Wie zijn de meest toegewijde luisteraars?
</x-slot:under-title>
<div class="px-2 text-sm space-y-4 dark:text-gray-200">
<!-- Period Navigation -->
<div class="flex flex-wrap gap-2 mb-6">
<a href="{{ route('radio.leaderboard', ['period' => 'all']) }}"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 transform hover:scale-105
{{ $period === 'all'
? 'bg-gradient-to-r from-yellow-400 to-yellow-500 text-gray-900 shadow-lg shadow-yellow-500/30'
: 'bg-gray-700/50 text-gray-200 hover:bg-gray-600' }}">
🌟 Alles
</a>
<a href="{{ route('radio.leaderboard', ['period' => 'weekly']) }}"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 transform hover:scale-105
{{ $period === 'weekly'
? 'bg-gradient-to-r from-yellow-400 to-yellow-500 text-gray-900 shadow-lg shadow-yellow-500/30'
: 'bg-gray-700/50 text-gray-200 hover:bg-gray-600' }}">
📅 Deze Week
</a>
<a href="{{ route('radio.leaderboard', ['period' => 'monthly']) }}"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 transform hover:scale-105
{{ $period === 'monthly'
? 'bg-gradient-to-r from-yellow-400 to-yellow-500 text-gray-900 shadow-lg shadow-yellow-500/30'
: 'bg-gray-700/50 text-gray-200 hover:bg-gray-600' }}">
📆 Deze Maand
</a>
</div>
<!-- Top 3 Podium -->
@if(!empty($users) && count($users) >= 3)
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<!-- 2nd Place -->
<div class="order-1 md:order-1">
<div class="bg-gradient-to-b from-gray-700 to-gray-800 rounded-xl p-6 text-center border border-gray-600/50 shadow-xl transform hover:scale-105 transition-all duration-300 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-gray-500/10 to-transparent"></div>
<div class="relative">
<div class="absolute -top-6 left-1/2 -translate-x-1/2">
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-gray-300 to-gray-400 flex items-center justify-center text-2xl shadow-lg">
🥈
</div>
</div>
<div class="mt-8 mb-3">
<img src="{{ $users[1]['avatar'] ?? 'https://ui-avatars.com/api/?name=' . urlencode($users[1]['username']) . '&size=128&background=6b7280&color=ffffff' }}"
alt="{{ $users[1]['username'] }}"
class="w-24 h-24 rounded-full mx-auto border-4 border-gray-500 shadow-lg">
</div>
<h4 class="font-bold text-xl text-white mb-1">{{ $users[1]['username'] }}</h4>
<div class="inline-flex items-center gap-1 px-4 py-1.5 rounded-full bg-gray-600 text-gray-200 font-medium text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
{{ number_format($users[1]['points']) }}
</div>
</div>
</div>
</div>
<!-- 1st Place -->
<div class="order-2">
<div class="bg-gradient-to-b from-yellow-500/20 to-gray-800 rounded-xl p-6 text-center border-2 border-yellow-500/50 shadow-xl shadow-yellow-500/20 transform hover:scale-105 transition-all duration-300 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-yellow-500/10 to-transparent"></div>
<div class="absolute -top-8 left-1/2 -translate-x-1/2">
<div class="w-20 h-20 rounded-full bg-gradient-to-br from-yellow-300 to-yellow-500 flex items-center justify-center text-3xl shadow-lg shadow-yellow-500/50 animate-pulse">
🥇
</div>
</div>
<div class="mt-10 mb-3">
<img src="{{ $users[0]['avatar'] ?? 'https://ui-avatars.com/api/?name=' . urlencode($users[0]['username']) . '&size=128&background=eeb425&color=000000' }}"
alt="{{ $users[0]['username'] }}"
class="w-28 h-28 rounded-full mx-auto border-4 border-yellow-400 shadow-lg shadow-yellow-500/30">
</div>
<h4 class="font-bold text-xl text-yellow-400 mb-1">{{ $users[0]['username'] }}</h4>
<div class="inline-flex items-center gap-1 px-4 py-1.5 rounded-full bg-yellow-500/20 text-yellow-400 font-medium text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
{{ number_format($users[0]['points']) }}
</div>
</div>
</div>
<!-- 3rd Place -->
<div class="order-3 md:order-3">
<div class="bg-gradient-to-b from-orange-700/50 to-gray-800 rounded-xl p-6 text-center border border-orange-600/30 shadow-xl transform hover:scale-105 transition-all duration-300 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-orange-500/10 to-transparent"></div>
<div class="relative">
<div class="absolute -top-6 left-1/2 -translate-x-1/2">
<div class="w-14 h-14 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-xl shadow-lg">
🥉
</div>
</div>
<div class="mt-6 mb-3">
<img src="{{ $users[2]['avatar'] ?? 'https://ui-avatars.com/api/?name=' . urlencode($users[2]['username']) . '&size=128&background=ea580c&color=ffffff' }}"
alt="{{ $users[2]['username'] }}"
class="w-20 h-20 rounded-full mx-auto border-4 border-orange-500 shadow-lg">
</div>
<h4 class="font-bold text-lg text-orange-400 mb-1">{{ $users[2]['username'] }}</h4>
<div class="inline-flex items-center gap-1 px-4 py-1.5 rounded-full bg-orange-500/20 text-orange-400 font-medium text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
{{ number_format($users[2]['points']) }}
</div>
</div>
</div>
</div>
</div>
@endif
<!-- Leaderboard List -->
<div class="bg-gray-800/50 backdrop-blur-sm rounded-xl overflow-hidden border border-gray-700/50 shadow-xl">
<div class="px-6 py-4 border-b border-gray-700/50 bg-gradient-to-r from-gray-800/80 to-gray-800/40">
<h4 class="font-bold text-white flex items-center gap-3">
<span class="w-8 h-8 rounded-lg bg-[#eeb425]/20 flex items-center justify-center">
<svg class="w-5 h-5 text-[#eeb425]" fill="currentColor" viewBox="0 0 24 24">
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/>
</svg>
</span>
Alle Luisteraars
<span class="ml-auto text-sm font-normal text-gray-400">({{ count($users) }})</span>
</h4>
</div>
<div class="max-h-[500px] overflow-y-auto custom-scrollbar">
@forelse($users as $user)
<div class="flex items-center gap-4 px-6 py-3 border-b border-gray-700/30 last:border-b-0 hover:bg-gray-700/30 transition-all duration-200 group">
<!-- Rank -->
<div class="w-8 flex-shrink-0">
@if($user['rank'] <= 3)
<div class="flex items-center justify-center w-8 h-8 rounded-full font-bold text-sm
{{ $user['rank'] === 1 ? 'bg-gradient-to-br from-yellow-400 to-yellow-600 text-gray-900 shadow-lg shadow-yellow-500/30' :
($user['rank'] === 2 ? 'bg-gradient-to-br from-gray-300 to-gray-500 text-gray-900' :
'bg-gradient-to-br from-orange-400 to-orange-600 text-white') }}">
{{ $user['rank'] }}
</div>
@else
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-700/50 text-gray-400 text-sm font-medium">
{{ $user['rank'] }}
</div>
@endif
</div>
<!-- Avatar -->
<div class="relative flex-shrink-0">
<img src="{{ $user['avatar'] ?? 'https://ui-avatars.com/api/?name=' . urlencode($user['username']) . '&size=64&background=374151&color=eeb425' }}"
alt="{{ $user['username'] }}"
class="w-12 h-12 rounded-full border-2 border-gray-600 group-hover:border-[#eeb425] transition-colors duration-200">
@if($user['rank'] <= 3)
<div class="absolute -top-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center text-xs
{{ $user['rank'] === 1 ? 'bg-yellow-400' : ($user['rank'] === 2 ? 'bg-gray-300' : 'bg-orange-400') }}">
{{ $user['rank'] }}
</div>
@endif
</div>
<!-- Username -->
<div class="flex-1 min-w-0">
<div class="font-semibold text-white group-hover:text-[#eeb425] transition-colors duration-200 truncate">{{ $user['username'] }}</div>
@if($user['rank'] <= 3)
<div class="text-xs">
<span class="{{ $user['rank'] === 1 ? 'text-yellow-400' : ($user['rank'] === 2 ? 'text-gray-300' : 'text-orange-400') }}">
{{ $user['rank'] === 1 ? '👑 Kampioen' : ($user['rank'] === 2 ? '🥈 Runner-up' : '🥉 Top 3') }}
</span>
</div>
@endif
</div>
<!-- Points -->
<div class="flex items-center gap-3 flex-shrink-0">
<div class="px-3 py-1.5 rounded-lg bg-gradient-to-r from-[#eeb425]/10 to-[#eeb425]/5 border border-[#eeb425]/20">
<span class="text-[#eeb425] font-bold">{{ number_format($user['points']) }}</span>
<span class="text-[#eeb425]/60 text-xs ml-1">pts</span>
</div>
@if($user['rank'] === 1)
<span class="text-2xl">🏆</span>
@elseif($user['rank'] <= 10)
<span class="text-lg"></span>
@endif
</div>
</div>
@empty
<div class="text-center py-16 px-4">
<div class="relative inline-block mb-4">
<div class="w-24 h-24 rounded-full bg-gray-700/50 flex items-center justify-center text-5xl">
🎵
</div>
<div class="absolute -bottom-2 -right-2 w-10 h-10 rounded-full bg-[#eeb425] flex items-center justify-center text-xl">
🤔
</div>
</div>
<h4 class="text-xl font-bold text-white mb-2">{{ __('radio.no_listeners') }}</h4>
<p class="text-gray-400 mb-6 max-w-sm mx-auto">{{ __('radio.listen_to_earn') }}</p>
<a href="{{ route('radio.index') }}"
class="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-[#eeb425] to-yellow-500 text-gray-900 rounded-xl font-semibold hover:from-yellow-400 hover:to-yellow-400 transition-all duration-300 transform hover:scale-105 shadow-lg shadow-yellow-500/30">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
{{ __('radio.start_listening') }}
</a>
</div>
@endforelse
</div>
</div>
</div>
</x-content.content-card>
</div>
</div>
<!-- Sidebar -->
<div class="col-span-12 lg:col-span-3 lg:w-[110%] space-y-4 lg:-ml-[32px]">
<x-content.content-card icon="info" classes="border dark:border-gray-900 bg-gradient-to-br from-gray-800/50 to-gray-900/50">
<x-slot:title>
{{ __('radio.leaderboard_info') }}
</x-slot:title>
<x-slot:under-title>
{{ __('radio.how_it_works') }}
</x-slot:under-title>
<div class="px-2 text-sm space-y-4 dark:text-gray-200">
<p class="text-gray-300">{{ __('radio.points_auto') }}</p>
<div class="space-y-3 text-xs">
<div class="flex items-center gap-3 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
<span class="w-3 h-3 rounded-full bg-green-500 shadow-lg shadow-green-500/50"></span>
<span class="text-green-400">{{ __('radio.points_per_minute') }}</span>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<span class="w-3 h-3 rounded-full bg-yellow-500 shadow-lg shadow-yellow-500/50"></span>
<span class="text-yellow-400">{{ __('radio.max_points_per_day') }}</span>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
<span class="w-3 h-3 rounded-full bg-blue-500 shadow-lg shadow-blue-500/50"></span>
<span class="text-blue-400">{{ __('radio.leaderboard_updated') }}</span>
</div>
</div>
</div>
</x-content.content-card>
<x-content.content-card icon="trophy" classes="border dark:border-gray-900 bg-gradient-to-br from-[#eeb425]/10 to-transparent">
<x-slot:title>
{{ __('radio.quick_stats') }}
</x-slot:title>
<x-slot:under-title>
{{ __('radio.current_stand') }}
</x-slot:under-title>
<div class="px-2 text-sm space-y-4 dark:text-gray-200">
<div class="flex justify-between items-center p-3 rounded-lg bg-gray-700/30">
<span class="text-gray-400">{{ __('radio.active_listeners') }}</span>
<span class="font-bold text-[#eeb425] text-lg">{{ count($users) }}</span>
</div>
@if(!empty($users))
<div class="flex justify-between items-center p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<span class="text-gray-400">{{ __('radio.current_leader') }}</span>
<span class="font-bold text-yellow-400 flex items-center gap-2">
👑 {{ $users[0]['username'] }}
</span>
</div>
@endif
<div class="flex justify-between items-center p-3 rounded-lg bg-gray-700/30">
<span class="text-gray-400">{{ __('radio.current_period') }}</span>
<span class="font-semibold text-white">
{{ $period === 'weekly' ? __('radio.this_week') : ($period === 'monthly' ? __('radio.this_month') : __('radio.total')) }}
</span>
</div>
</div>
</x-content.content-card>
<!-- Top 3 Compact -->
@if(!empty($users) && count($users) >= 3)
<x-content.content-card icon="trophy" classes="border dark:border-gray-900">
<x-slot:title>
Top 3
</x-slot:title>
<x-slot:under-title>
De beste luisteraars
</x-slot:under-title>
<div class="px-2 space-y-3">
@foreach(array_slice($users, 0, 3) as $index => $user)
<div class="flex items-center gap-3 p-3 rounded-lg {{ $index === 0 ? 'bg-yellow-500/10 border border-yellow-500/20' : 'bg-gray-700/30' }}">
<div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm
{{ $index === 0 ? 'bg-gradient-to-br from-yellow-400 to-yellow-600 text-gray-900' :
($index === 1 ? 'bg-gradient-to-br from-gray-300 to-gray-500 text-gray-900' :
'bg-gradient-to-br from-orange-400 to-orange-600 text-white') }}">
{{ $index + 1 }}
</div>
<img src="{{ $user['avatar'] ?? 'https://ui-avatars.com/api/?name=' . urlencode($user['username']) . '&size=64&background=374151&color=eeb425' }}"
alt="{{ $user['username'] }}"
class="w-10 h-10 rounded-full border-2 border-gray-600">
<div class="flex-1 min-w-0">
<div class="font-semibold text-white truncate">{{ $user['username'] }}</div>
<div class="text-xs text-[#eeb425]">{{ number_format($user['points']) }} pts</div>
</div>
<span class="text-xl">
{{ $index === 0 ? '🏆' : ($index === 1 ? '🥈' : '🥉') }}
</span>
</div>
@endforeach
</div>
</x-content.content-card>
@endif
</div>
</x-app-layout>
+83
View File
@@ -0,0 +1,83 @@
@extends('layouts.app')
@section('title', __('radio.requests') . ' - ' . config('app.name'))
@section('content')
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-white">
<h2 class="text-2xl font-bold mb-4">{{ __('radio.requests') }}</h2>
@if(session('success'))
<div class="bg-green-500 text-white p-4 rounded mb-4">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="bg-red-500 text-white p-4 rounded mb-4">
{{ session('error') }}
</div>
@endif
<form action="{{ route('radio.requests.store') }}" method="POST" class="mb-8">
@csrf
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="artist" class="block text-sm font-medium text-gray-300">{{ __('radio.artist') }}</label>
<input type="text" name="artist" id="artist" required class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label for="title" class="block text-sm font-medium text-gray-300">{{ __('radio.title') }}</label>
<input type="text" name="title" id="title" required class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white focus:border-indigo-500 focus:ring-indigo-500">
</div>
</div>
<div class="mt-4">
<label for="message" class="block text-sm font-medium text-gray-300">{{ __('radio.message') }}</label>
<input type="text" name="message" id="message" class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div class="mt-4">
<button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
{{ __('radio.submit_request') }}
</button>
</div>
</form>
@if(isset($recentRequests) && count($recentRequests) > 0)
<h3 class="text-xl font-bold mb-4">{{ __('radio.queue') }}</h3>
<div class="overflow-x-auto">
<table class="min-w-full bg-gray-700 rounded-lg overflow-hidden">
<thead class="bg-gray-600">
<tr>
<th class="py-3 px-4 text-left">{{ __('radio.artist') }}</th>
<th class="py-3 px-4 text-left">{{ __('radio.title') }}</th>
<th class="py-3 px-4 text-left">{{ __('radio.requested_by') }}</th>
<th class="py-3 px-4 text-left">{{ __('radio.votes') }}</th>
<th class="py-3 px-4 text-left">{{ __('radio.vote') }}</th>
</tr>
</thead>
<tbody>
@foreach($recentRequests as $request)
<tr class="border-b border-gray-600">
<td class="py-3 px-4">{{ $request->artist }}</td>
<td class="py-3 px-4">{{ $request->title }}</td>
<td class="py-3 px-4">{{ $request->user->username ?? __('radio.unknown') }}</td>
<td class="py-3 px-4">{{ $request->votes }}</td>
<td class="py-3 px-4">
<form action="{{ route('radio.requests.vote', $request) }}" method="POST">
@csrf
<button type="submit" class="text-indigo-400 hover:text-indigo-300">{{ __('radio.vote') }}</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-gray-400">{{ __('radio.no_requests') }}</p>
@endif
</div>
</div>
</div>
@endsection
+44
View File
@@ -0,0 +1,44 @@
@extends('layouts.app')
@section('title', __('radio.timetable') . ' - ' . config('app.name'))
@section('content')
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-white">
<h2 class="text-2xl font-bold mb-6">{{ __('radio.timetable') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-7 gap-4">
@php
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
@endphp
@foreach($days as $day)
<div class="bg-gray-700 rounded-lg overflow-hidden">
<div class="bg-gray-600 p-3 text-center font-bold">
{{ __('radio.days.' . $day) }}
</div>
<div class="p-4 space-y-3">
@if(isset($schedule[$day]) && count($schedule[$day]) > 0)
@foreach($schedule[$day] as $slot)
<div class="bg-gray-800 p-3 rounded border border-gray-600">
<div class="text-sm text-indigo-400 font-semibold">
{{ $slot->start_time->format('H:i') }} - {{ $slot->end_time->format('H:i') }}
</div>
<div class="font-bold">{{ $slot->user->username ?? __('radio.unknown') }}</div>
<div class="text-xs text-gray-400 truncate">{{ $slot->show_name }}</div>
</div>
@endforeach
@else
<div class="text-center text-gray-500 text-sm py-4">
{{ __('radio.no_shows') }}
</div>
@endif
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
@endsection
+71
View File
@@ -0,0 +1,71 @@
@extends('layouts.app')
@section('title', __('radio.shouts') . ' - ' . config('app.name'))
@section('content')
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-white">
<h2 class="text-2xl font-bold mb-4">{{ __('radio.shouts') }}</h2>
@if(session('success'))
<div class="bg-green-500 text-white p-4 rounded mb-4">
{{ session('success') }}
</div>
@endif
<form action="{{ route('radio.shouts.store') }}" method="POST" class="mb-8">
@csrf
<div>
<label for="message" class="block text-sm font-medium text-gray-300">{{ __('radio.shout_message') }}</label>
<textarea name="message" id="message" required rows="3" maxlength="280" placeholder="{{ __('radio.shout_placeholder') }}" class="mt-1 block w-full rounded-md bg-gray-700 border-gray-600 text-white focus:border-indigo-500 focus:ring-indigo-500"></textarea>
<p class="text-xs text-gray-400 mt-1">{{ __('radio.max_chars') }}</p>
</div>
<div class="mt-4">
<button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
{{ __('radio.send_shout') }}
</button>
</div>
</form>
<div id="shouts-container" class="space-y-4">
{{-- Shouts will be loaded via API --}}
<div class="text-center text-gray-400">{{ __('radio.loading') }}</div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function loadShouts() {
fetch('/api/radio/shouts')
.then(response => response.json())
.then(data => {
const container = document.getElementById('shouts-container');
if (data.shouts && data.shouts.length > 0) {
container.innerHTML = data.shouts.map(shout => `
<div class="bg-gray-700 p-4 rounded-lg">
<div class="flex justify-between items-center mb-2">
<span class="font-bold text-indigo-400">${shout.username}</span>
<span class="text-xs text-gray-400">${shout.created_at}</span>
</div>
<p class="text-gray-300">${shout.message}</p>
</div>
`).join('');
} else {
container.innerHTML = "<p class='text-center text-gray-400'>{{ __('radio.no_shouts') }}</p>";
}
})
.catch(() => {
document.getElementById('shouts-container').innerHTML = "<p class='text-center text-red-400'>{{ __('radio.load_error') }}</p>";
});
}
document.addEventListener('DOMContentLoaded', function() {
loadShouts();
setInterval(loadShouts, 30000); // Reload every 30 seconds
});
</script>
@endpush
@endsection
@@ -0,0 +1,22 @@
@props(['icon' => '', 'classes' => ''])
<div
class="w-full flex flex-col gap-y-4 rounded overflow-hidden bg-white pb-3 shadow-sm {{ $classes }}">
<div class="flex gap-x-2 border-b bg-gray-50 p-3">
<div
class="max-w-[50px] max-h-[50px] min-w-[50px] min-h-[50px] rounded-full relative flex items-center justify-center {{ $icon }}">
</div>
<div class="flex flex-col justify-center text-sm">
<p class="font-semibold text-black">{{ $title }}</p>
@if(isset($underTitle))
<p>{{ $underTitle }}</p>
@endif
</div>
</div>
<section class="px-3 text-[14px]">
{{ $slot }}
</section>
</div>
+11
View File
@@ -0,0 +1,11 @@
@props(['primaryColor', 'secondaryColor'])
<div class="flex h-[45px] col-span-4 sm:col-span-2 md:col-span-1">
<div class="w-1/3 {{ $secondaryColor }} rounded-l flex items-center justify-center">
{{ $icon }}
</div>
<div class="p-2 rounded rounded-l-none {{ $primaryColor }} w-2/3 font-semibold flex justify-center items-center">
{{ $slot }}
</div>
</div>
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
</svg>

After

Width:  |  Height:  |  Size: 478 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>

After

Width:  |  Height:  |  Size: 218 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>

After

Width:  |  Height:  |  Size: 487 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
</svg>

After

Width:  |  Height:  |  Size: 363 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 11.25v8.25a1.5 1.5 0 0 1-1.5 1.5H5.25a1.5 1.5 0 0 1-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 1 0 9.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1 1 14.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z" />
</svg>

After

Width:  |  Height:  |  Size: 518 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5">
<path
d="M11.47 3.84a.75.75 0 011.06 0l8.69 8.69a.75.75 0 101.06-1.06l-8.689-8.69a2.25 2.25 0 00-3.182 0l-8.69 8.69a.75.75 0 001.061 1.06l8.69-8.69z" />
<path
d="M12 5.432l8.159 8.159c.03.03.06.058.091.086v6.198c0 1.035-.84 1.875-1.875 1.875H15a.75.75 0 01-.75-.75v-4.5a.75.75 0 00-.75-.75h-3a.75.75 0 00-.75.75V21a.75.75 0 01-.75.75H5.625a1.875 1.875 0 01-1.875-1.875v-6.198a2.29 2.29 0 00.091-.086L12 5.43z" />
</svg>

After

Width:  |  Height:  |  Size: 541 B

+4
View File
@@ -0,0 +1,4 @@
<svg {{ $attributes }} fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>

After

Width:  |  Height:  |  Size: 300 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
<path fill-rule="evenodd"
d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z"
clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 354 B

+193
View File
@@ -0,0 +1,193 @@
<div x-data="radioPlayer()" x-init="initPlayer()"
class="radio-player-widget"
:class="{ 'radio-player-visible': isVisible, 'radio-player-hidden': !isVisible }"
x-show="showWidget"
style="display: none;">
<!-- Toggle Button -->
<button
x-show="!isExpanded"
@click="toggleExpand()"
class="fixed bottom-4 right-4 z-50 w-14 h-14 rounded-full bg-gradient-to-r from-amber-500 to-amber-600 text-white shadow-lg hover:shadow-amber-500/30 flex items-center justify-center transition-all hover:scale-110"
>
<svg class="w-6 h-6 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
</svg>
</button>
<!-- Expanded Player -->
<div
x-show="isExpanded"
class="fixed bottom-4 right-4 z-50 w-96 rounded-2xl shadow-2xl overflow-hidden bg-gray-900"
>
<!-- Header -->
<div class="bg-gradient-to-r from-amber-500 to-amber-600 p-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-white rounded-full animate-pulse"></div>
<span class="text-white font-semibold">RADIO</span>
</div>
<div class="flex items-center gap-2">
<span x-text="listenerCount" class="text-white/80 text-sm">--</span>
<span class="text-white/60 text-xs">luisteraars</span>
<button @click="toggleExpand()" class="text-white/80 hover:text-white">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
</div>
</div>
<!-- Content -->
<div class="p-4 text-white">
<!-- DJ Info -->
<div x-show="currentDJ" class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center overflow-hidden">
<img x-bind:src="djAvatar" x-bind:alt="djName" class="w-full h-full object-cover">
</div>
<div>
<p class="text-xs text-amber-400">PRESENTATOR</p>
<p x-text="djName" class="font-semibold">--</p>
</div>
</div>
<!-- Now Playing -->
<div class="bg-gray-800 rounded-lg p-3 mb-4">
<p class="text-xs text-gray-400 mb-1">NU DRAAIT</p>
<p x-text="trackTitle" class="font-medium truncate">Radio</p>
<p x-show="trackArtist" x-text="trackArtist" class="text-sm text-gray-400 truncate"></p>
</div>
<!-- Controls -->
<div class="flex items-center justify-between gap-4">
<button @click="togglePlay()" class="w-14 h-14 rounded-full bg-amber-500 hover:bg-amber-400 flex items-center justify-center transition shadow-lg">
<svg x-show="!isPlaying" class="w-6 h-6 ml-1" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
<svg x-show="isPlaying" class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</button>
<div class="flex-1 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M18.364 5.636a9 9 0 010 12.728"/>
</svg>
<input type="range" x-model="volume" min="0" max="100" class="flex-1 h-1 bg-gray-700 rounded-lg appearance-none cursor-pointer">
</div>
<a href="/community/radio" class="text-amber-400 hover:text-amber-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
</a>
</div>
<audio x-ref="audioPlayer" preload="none">
<source x-bind:src="streamUrl" type="audio/mpeg">
</audio>
</div>
</div>
</div>
<script>
function radioPlayer() {
return {
showWidget: false,
showOnRadioPage: false,
showGlobal: false,
isVisible: true,
isExpanded: false,
isPlaying: false,
streamUrl: '',
volume: 80,
listenerCount: '--',
trackTitle: 'Radio',
trackArtist: '',
currentDJ: null,
djName: '--',
djAvatar: '',
initPlayer() {
this.checkVisibility();
this.loadSettings();
setInterval(() => this.updateListeners(), 30000);
setInterval(() => this.updateNowPlaying(), 15000);
this.updateVisibilityByUrl();
},
checkVisibility() {
if (!this.showWidget) {
this.showWidget = false;
return;
}
if (this.showGlobal) {
this.showWidget = true;
} else if (this.showOnRadioPage) {
this.updateVisibilityByUrl();
} else {
this.showWidget = false;
}
},
updateVisibilityByUrl() {
const path = window.location.pathname;
this.showWidget = path === '/community/radio' || path.startsWith('/community/radio');
},
loadSettings() {
fetch('/api/radio/config')
.then(r => r.json())
.then(data => {
if (data.enabled && data.stream_url && data.widget_enabled) {
this.streamUrl = data.streamUrl || data.stream_url;
this.showGlobal = data.widget_show_globally || false;
this.checkVisibility();
} else {
this.showWidget = false;
}
})
.catch(() => {
this.showWidget = false;
});
},
toggleExpand() {
this.isExpanded = !this.isExpanded;
},
togglePlay() {
const audio = this.$refs.audioPlayer;
if (this.isPlaying) {
audio.pause();
this.isPlaying = false;
} else {
audio.play().catch(() => {});
this.isPlaying = true;
}
},
updateListeners() {
fetch('/api/radio/listeners')
.then(r => r.json())
.then(data => {
this.listenerCount = data.count.toLocaleString();
})
.catch(() => {
this.listenerCount = '--';
});
},
updateNowPlaying() {
fetch('/api/radio/now-playing')
.then(r => r.json())
.then(data => {
if (data.title) {
this.trackTitle = data.title;
this.trackArtist = data.artist || '';
}
})
.catch(() => {});
}
}
}
</script>
@@ -0,0 +1,13 @@
<?php
use Livewire\Component;
new class extends Component
{
//
};
?>
<div>
{{-- Simplicity is an acquired taste. - Katharine Gerould --}}
</div>
@@ -0,0 +1 @@
@livewire('emulator-log-viewer')
@@ -0,0 +1,230 @@
<x-dynamic-component
:component="$getFieldWrapperView()"
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:hint-icon="$getHintIcon()"
:required="$isRequired()"
:state-path="$getStatePath()"
class="relative z-0"
>
<div x-data="{ state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }}, initialized: false, editorChanged: false }"
x-init="(() => {
window.addEventListener('DOMContentLoaded', () => initCKEditor())
$nextTick(() => initCKEditor())
const initCKEditor = () => {
if(initialized) return
if(typeof CKEDITOR === undefined || !$refs.ckeditor) {
console.error('[CKEDITOR] not found or [CKEDITOR element] not found')
return
}
CKEDITOR.ClassicEditor.create($refs.ckeditor, {
toolbar: {
items: [
'exportPDF',
'exportWord',
'|',
'findAndReplace',
'selectAll',
'heading',
'|',
'bold',
'italic',
'strikethrough',
'underline',
'code',
'subscript',
'superscript',
'removeFormat',
'|',
'bulletedList',
'numberedList',
'outdent',
'indent',
'|',
'undo',
'redo',
'fontSize',
'fontFamily',
'fontColor',
'fontBackgroundColor',
'highlight',
'|',
'alignment',
'link',
'insertImage',
'blockQuote',
'insertTable',
'mediaEmbed',
'codeBlock',
'htmlEmbed',
'specialCharacters',
'horizontalLine',
'pageBreak',
'|',
'sourceEditing',
],
shouldNotGroupWhenFull: true,
},
list: {
properties: {
styles: true,
startIndex: true,
reversed: true,
},
},
heading: {
options: [
{
model: 'paragraph',
title: 'Paragraph',
class: 'ck-heading_paragraph',
},
{
model: 'heading1',
view: 'h1',
title: 'Heading 1',
class: 'ck-heading_heading1',
},
{
model: 'heading2',
view: 'h2',
title: 'Heading 2',
class: 'ck-heading_heading2',
},
{
model: 'heading3',
view: 'h3',
title: 'Heading 3',
class: 'ck-heading_heading3',
},
{
model: 'heading4',
view: 'h4',
title: 'Heading 4',
class: 'ck-heading_heading4',
},
{
model: 'heading5',
view: 'h5',
title: 'Heading 5',
class: 'ck-heading_heading5',
},
{
model: 'heading6',
view: 'h6',
title: 'Heading 6',
class: 'ck-heading_heading6',
},
],
},
placeholder: '. . .',
fontFamily: {
options: [
'default',
'Arial, Helvetica, sans-serif',
'Courier New, Courier, monospace',
'Georgia, serif',
'Lucida Sans Unicode, Lucida Grande, sans-serif',
'Tahoma, Geneva, sans-serif',
'Times New Roman, Times, serif',
'Trebuchet MS, Helvetica, sans-serif',
'Verdana, Geneva, sans-serif',
'Montserrat, sans-serif'
],
supportAllValues: true,
},
htmlSupport: {
allow: [
{
name: /.*/,
attributes: true,
classes: true,
styles: true,
},
],
},
htmlEmbed: {
showPreviews: true,
},
link: {
decorators: {
addTargetToExternalLinks: true,
defaultProtocol: 'https://',
toggleDownloadable: {
mode: 'manual',
label: 'Downloadable',
attributes: {
download: 'file',
},
},
},
},
removePlugins: [
'CKBox',
'CKFinder',
'EasyImage',
'RealTimeCollaborativeComments',
'RealTimeCollaborativeTrackChanges',
'RealTimeCollaborativeRevisionHistory',
'PresenceList',
'Comments',
'TrackChanges',
'TrackChangesData',
'RevisionHistory',
'Pagination',
'WProofreader',
'MathType',
],
}).then(editor => {
if(state) editor.setData(state)
editor.model.document.on('change:data', () => { editorChanged = true })
editor.ui.focusTracker.on('change:isFocused', (evt, name, isFocused) => {
if(isFocused || !editorChanged) return
state = editor.getData()
editorChanged = false
})
});
initialized = true
}
})()"
x-cloak
wire:ignore
>
@unless($isDisabled())
<input
id="ck-editor-{{ $getId() }}"
type="hidden"
x-ref="ckeditor"
placeholder="{{ $getPlaceholder() }}"
>
@else
<div
x-html="state"
style="font-size: 13px"
@class([
'prose ck-content block w-full max-w-none rounded-lg border border-gray-300 bg-white p-3 opacity-70 shadow-xs transition duration-75',
'dark:prose-invert dark:bg-gray-700 dark:border-gray-600 dark:text-white' => true,
])
></div>
@endunless
</div>
</x-dynamic-component>
@once
@push('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/35.4.0/super-build/ckeditor.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
@endpush
@endonce
@@ -0,0 +1,39 @@
@props(['icons' => []])
<div x-data="{ open: false }" class="mt-6">
<div class="flex items-center justify-between mb-2">
<h3 class="text-base font-medium">Icon picker</h3>
<button
type="button"
class="fi-btn fi-btn-size-md fi-btn-color-gray fi-btn-variant-outline"
@click="open = !open"
>
<span x-text="open ? 'Hide icons' : 'Select icon'"></span>
</button>
</div>
<template x-if="open">
<div
class="grid gap-2 mt-2"
style="grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));"
>
@foreach($icons as $icon)
<button
type="button"
class="rounded border p-1 bg-white dark:bg-gray-900 flex items-center justify-center border-gray-200 dark:border-gray-700 hover:border-primary-400"
@click="$wire.setIconFromPicker({{ $icon['id'] }})"
title="Icon {{ $icon['id'] }}"
>
<img
src="{{ $icon['url'] }}"
alt="icon {{ $icon['id'] }}"
class="h-8 w-8 object-contain"
loading="lazy"
onerror="this.onerror=null;this.src='{{ $icon['fallback'] }}';"
style="image-rendering: pixelated; image-rendering: crisp-edges;"
/>
</button>
@endforeach
</div>
</template>
</div>
@@ -0,0 +1,13 @@
@props(['getUrl' => null, 'fallbackUrl' => null])
<div class="flex items-center gap-3">
<div class="text-sm text-gray-600 dark:text-gray-300">Current icon:</div>
<img
src="{{ is_callable($getUrl) ? $getUrl() : $getUrl }}"
alt=""
class="h-8 w-8 object-contain"
loading="lazy"
onerror="this.onerror=null;this.src='{{ $fallbackUrl }}';"
style="image-rendering: pixelated; image-rendering: crisp-edges;"
/>
</div>
@@ -0,0 +1,57 @@
@php
use Filament\Infolists\Components\IconEntry\IconEntrySize;
@endphp
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
<div
{{
$attributes
->merge($getExtraAttributes(), escape: false)
->class([
'absolute flex items-center justify-center w-6 h-6 bg-gray-200 rounded-full -start-3 ring-4 ring-white dark:bg-gray-700 dark:ring-gray-900',
])
}}
>
@if (count($arrayState = \Illuminate\Support\Arr::wrap($getState())))
@foreach ($arrayState as $state)
@if ($icon = $getIcon($state))
@php
$color = $getColor($state) ?? 'gray';
$size = $getSize($state) ?? IconEntrySize::Large;
@endphp
<x-filament::icon
:icon="$icon"
@class([
'fi-in-icon-item',
match ($size) {
IconEntrySize::ExtraSmall, 'xs' => 'fi-in-icon-item-size-xs h-3 w-3',
IconEntrySize::Small, 'sm' => 'fi-in-icon-item-size-sm h-4 w-4',
IconEntrySize::Medium, 'md' => 'fi-in-icon-item-size-md h-5 w-5',
IconEntrySize::Large, 'lg' => 'fi-in-icon-item-size-lg h-6 w-6',
IconEntrySize::ExtraLarge, 'xl' => 'fi-in-icon-item-size-xl h-7 w-7',
IconEntrySize::TwoExtraLarge, IconEntrySize::ExtraExtraLarge, '2xl' => 'fi-in-icon-item-size-2xl h-8 w-8',
default => $size,
},
match ($color) {
'gray' => 'fi-color-gray text-gray-400 dark:text-gray-500',
default => 'fi-color-custom text-custom-500 dark:text-custom-400',
},
])
@style([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 500],
alias: 'infolists::components.icon-entry.item',
) => $color !== 'gray',
])
/>
@endif
@endforeach
@elseif (($placeholder = $getPlaceholder()) !== null)
<div class="text-gray-500 text-sm p-4">
{{ $placeholder }}
</div>
@endif
</div>
</x-dynamic-component>
@@ -0,0 +1,7 @@
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
<div>
{{ $getModifiedState() ?? (!is_array($getState()) ? $getState() ?? $getPlaceholder() : null) }}
</div>
</x-dynamic-component>
@@ -0,0 +1,53 @@
@php
$isContained = $isContained();
@endphp
<div x-data="{}"
x-load-css="[@js(\Filament\Support\Facades\FilamentAsset::getStyleHref('activitylog-styles', package: 'rmsramos/activitylog'))]"
>
</div>
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
<div
{{
$attributes
->merge([
'id' => $getId(),
], escape: false)
->merge($getExtraAttributes(), escape: false)
->class([
'fi-in-repeatable',
'fi-contained' => $isContained,
])
}}
>
@if (count($childComponentContainers = $getChildComponentContainers()))
<ol class="relative border-gray-200 border-s dark:border-gray-700">
<x-filament-schemas::grid
:default="$getGridColumns('default')"
:sm="$getGridColumns('sm')"
:md="$getGridColumns('md')"
:lg="$getGridColumns('lg')"
:xl="$getGridColumns('xl')"
:two-xl="$getGridColumns('2xl')"
class="gap-2"
>
@foreach ($childComponentContainers as $container)
<li
@class([
'mb-4 ms-6',
'fi-in-repeatable-item block',
'rounded-xl bg-white p-4 shadow-xs ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10' => $isContained,
])
>
{{ $container }}
</li>
@endforeach
</x-filament-schemas::grid>
</ol>
@elseif (($placeholder = $getPlaceholder()) !== null)
<div class="text-gray-500 text-sm p-4">
{{ $placeholder }}
</div>
@endif
</div>
</x-dynamic-component>
@@ -0,0 +1,13 @@
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
<div
{{
$attributes
->merge($getExtraAttributes(), escape: false)
->class(['fi-in-text w-full -mt-6'])
}}
>
{{ $getModifiedState() }}
</div>
</x-dynamic-component>
+3
View File
@@ -0,0 +1,3 @@
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
@@ -0,0 +1,3 @@
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
+93
View File
@@ -0,0 +1,93 @@
<x-filament::page>
<div class="flex items-center justify-between gap-2">
<div class="w-full mr-2">
{{ $this->search }}
</div>
@if(config('filament-log-manager.allow_delete'))
<div class="w-auto ml-2">
<x-filament::button
x-on:click="window.dispatchEvent(new CustomEvent('open-modal', { detail: { id: 'filament-log-manager-delete-log-file-modal' } }));"
:disabled="is_null($this->logFile)"
type="button"
color="danger"
>
{{ __('filament-log-manager::translations.delete') }}
</x-filament::button>
</div>
@endif
@if(config('filament-log-manager.allow_download'))
<div class="w-auto ml-2">
<x-filament::button
wire:click="download"
:disabled="is_null($this->logFile)"
type="button"
color="primary"
>
{{ __('filament-log-manager::translations.download') }}
</x-filament::button>
</div>
@endif
</div>
<hr class="dark:border-gray-700">
<div>
<div>
<div x-data="{ isCardOpen: null }" class="flex flex-col">
@forelse($this->getLogs() as $key => $log)
<div
class="rounded-xl relative mb-2 py-3 px-3 bg-{{ $log['level_class'] }}"
:class="{'no-bottom-radius mb-0': isCardOpen == {{$key}}}"
>
<a
@click="isCardOpen = isCardOpen == {{$key}} ? null : {{$key}} "
style="cursor: pointer;"
class="block overflow-hidden rounded-t-xl text-white"
>
<span>[{{ $log['date'] }}]</span>
{{ Str::limit($log['text'], 100) }}
</a>
</div>
<div x-show="isCardOpen=={{$key}}" class="mb-2 px-4 py-4 bg-white dark:bg-gray-800 text-gray-900 dark:text-white rounded-xl no-top-radius">
<div class="space-y-2">
<p>{{$log['text']}}</p>
@if(!empty($log['stack']))
<div class="bg-gray-100 dark:bg-gray-900 !mt-4 p-4 text-sm opacity-40">
<pre style="overflow-x: scroll;"><code>{{ trim($log['stack']) }}</code></pre>
</div>
@endif
</div>
</div>
@empty
<h3 class="text-center">{{ __('filament-log-manager::translations.no_logs') }}</h3>
@endforelse
</div>
</div>
</div>
<x-filament::modal id="filament-log-manager-delete-log-file-modal">
<x-slot name="heading">
{{ __('filament-log-manager::translations.modal_delete_heading') }}
</x-slot>
<x-slot name="description">
{{ __('filament-log-manager::translations.modal_delete_subheading') }}
</x-slot>
<x-slot name="footerActions">
<x-filament::button
type="button"
x-on:click="isOpen = false"
color="secondary"
outlined="true"
class="filament-page-modal-button-action"
>
{{ __('filament-log-manager::translations.modal_delete_action_cancel') }}
</x-filament::button>
<x-filament::button
wire:click="delete"
x-on:click="isOpen = false"
type="button"
color="danger"
class="filament-page-modal-button-action"
>
{{ __('filament-log-manager::translations.modal_delete_action_confirm') }}
</x-filament::button>
</x-slot>
</x-filament::modal>
</x-filament::page>
@@ -0,0 +1,14 @@
<x-filament::page>
<div class="space-y-4">
<h2 class="text-lg font-bold">Upload a New Badge</h2>
{{-- Render the form --}}
<form wire:submit.prevent="save" class="space-y-4">
{{ $this->form }}
<x-filament::button type="submit">
Upload Badge
</x-filament::button>
</form>
</div>
</x-filament::page>
@@ -0,0 +1,3 @@
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
@@ -0,0 +1,3 @@
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
@@ -0,0 +1,7 @@
<?php
?>
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
@@ -0,0 +1,7 @@
<?php
?>
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
@@ -0,0 +1,7 @@
<?php
?>
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
@@ -0,0 +1,7 @@
<?php
?>
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
@@ -0,0 +1,7 @@
<?php
?>
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
@@ -0,0 +1,7 @@
<?php
?>
<x-filament-panels::page>
{{ $this->form }}
</x-filament-panels::page>
+117
View File
@@ -0,0 +1,117 @@
<x-filament-panels::page>
{{ $this->form }}
<div class="mt-8 space-y-8">
@if($this->blockedCountries->isNotEmpty())
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<h3 class="text-lg font-semibold mb-4">Geblokkeerde Landen</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
@foreach($this->blockedCountries as $country)
<div class="flex items-center justify-between bg-white dark:bg-gray-700 p-2 rounded">
<span>{{ $country->country_name }} ({{ $country->country_code }})</span>
<button
wire:click="removeBlockedCountry({{ $country->id }})"
class="text-red-500 hover:text-red-700"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
@endforeach
</div>
</div>
@endif
@if($this->whitelistedIps->isNotEmpty())
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<h3 class="text-lg font-semibold mb-4">Whitelisted</h3>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="text-left text-sm text-gray-500">
<th class="pb-2">IP</th>
<th class="pb-2">ASN</th>
<th class="pb-2">Land</th>
<th class="pb-2"></th>
</tr>
</thead>
<tbody>
@foreach($this->whitelistedIps as $item)
<tr class="border-t border-gray-200 dark:border-gray-600">
<td class="py-2">{{ $item->ip_address ?? '-' }}</td>
<td class="py-2">{{ $item->asn ?? '-' }}</td>
<td class="py-2">{{ $item->country_name ?? '-' }}</td>
<td class="py-2">
<button wire:click="removeFromWhitelist({{ $item->id }})" class="text-red-500 hover:text-red-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@if($this->blacklistedIps->isNotEmpty())
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<h3 class="text-lg font-semibold mb-4">Blacklisted</h3>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="text-left text-sm text-gray-500">
<th class="pb-2">IP</th>
<th class="pb-2">ASN</th>
<th class="pb-2">Land</th>
<th class="pb-2"></th>
</tr>
</thead>
<tbody>
@foreach($this->blacklistedIps as $item)
<tr class="border-t border-gray-200 dark:border-gray-600">
<td class="py-2">{{ $item->ip_address ?? '-' }}</td>
<td class="py-2">{{ $item->asn ?? '-' }}</td>
<td class="py-2">{{ $item->country_name ?? '-' }}</td>
<td class="py-2">
<button wire:click="removeFromBlacklist({{ $item->id }})" class="text-red-500 hover:text-red-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@if($this->blocklistStats && ($this->blocklistStats['total'] ?? 0) > 0)
<div class="bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg p-4 text-white">
<h3 class="text-lg font-bold mb-4">🛡️ Ultimate Blocklist (Gratis & Onbeperkt)</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-white/10 p-4 rounded text-center">
<div class="text-4xl font-bold">{{ number_format($this->blocklistStats['total'] ?? 0) }}</div>
<div class="text-sm text-white/80">Netwerken/Subnets</div>
<div class="text-xs text-white/60 mt-1">Dekking: Miljoenen IPs</div>
</div>
<div class="bg-white/10 p-4 rounded">
<div class="text-sm font-semibold mb-2">Inclusief blocklists:</div>
<div class="text-xs text-white/80 space-y-1">
<div> FireHol Level 1-4 (VPN/Proxy/TOR)</div>
<div> Spamhaus DROP/eDROP</div>
<div> DShield (hackers)</div>
<div> Emerging Threats (botnets)</div>
<div> Blocklist.de (bruteforce/mail/ssh/ftp/sip)</div>
</div>
</div>
</div>
</div>
@endif
</div>
</x-filament-panels::page>
@@ -0,0 +1,277 @@
<x-filament-panels::page class="!max-w-full !px-0">
<script>
window.catalogSelIds = [];
window.addEventListener('catalog-sel-update', (e) => {
window.catalogSelIds = Array.isArray(e.detail?.ids) ? e.detail.ids : [];
});
</script>
<div
x-data="{
h: 0,
leftWidth: 320,
resizing: false,
startX: 0,
startWidth: 0,
set() {
this.h = Math.max(320, window.innerHeight - 160);
},
init() {
this.set();
window.addEventListener('resize', () => this.set());
window.addEventListener('mousemove', e => this.doResize(e));
window.addEventListener('mouseup', () => this.stopResize());
const saved = localStorage.getItem('catalogEditorLeftWidth');
if (saved) this.leftWidth = parseInt(saved, 10);
window.addEventListener('scroll-to-page', e => {
const id = e.detail?.id;
if (!id) return;
const el = document.querySelector(`[data-page-id='${id}']`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('ring-2', 'ring-primary-500', 'rounded-md');
setTimeout(() => el.classList.remove('ring-2', 'ring-primary-500', 'rounded-md'), 1500);
}
});
},
startResize(e) {
this.resizing = true;
this.startX = e.clientX;
this.startWidth = this.leftWidth;
document.body.style.cursor = 'col-resize';
$refs.divider.classList.add('bg-primary-400');
},
stopResize() {
if (!this.resizing) return;
this.resizing = false;
document.body.style.cursor = '';
$refs.divider.classList.remove('bg-primary-400');
localStorage.setItem('catalogEditorLeftWidth', this.leftWidth);
},
doResize(e) {
if (!this.resizing) return;
const diff = e.clientX - this.startX;
this.leftWidth = Math.max(200, Math.min(700, this.startWidth + diff));
},
}"
x-init="init()"
:style="`
display:grid;
grid-template-columns:${leftWidth}px 8px 1fr;
height:${h}px;
gap:0;
width:100%;
overflow:hidden;
`"
class="relative select-none"
>
<div
class="dark:bg-gray-900 dark:border-gray-700"
style="
height:100%;
overflow:auto;
border:1px solid var(--gray-200);
border-radius:1rem;
padding:0.75rem;
background:var(--filament-color-white,#fff);
"
>
<div class="mb-3">
<x-filament::input.wrapper
class="w-full border border-gray-300 dark:border-gray-600 rounded-lg focus-within:ring-2 focus-within:ring-primary-500 transition"
>
<x-filament::input
wire:model.live.debounce.400ms="pageSearch"
placeholder="Search catalog pages or items..."
class="!border-0 !shadow-none !ring-0 !outline-none bg-transparent text-sm"
/>
<x-slot name="suffix">
<button
type="button"
wire:click="resetView"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-xs font-bold leading-none transition"
title="Reset to default view"
>
X
</button>
</x-slot>
</x-filament::input.wrapper>
</div>
@php
if ($pageSearch !== '') {
$search = trim($pageSearch);
$matchedPages = \App\Models\Game\Furniture\CatalogPage::query()
->where('caption', 'like', "%{$search}%")
->get();
$matchedItems = \App\Models\Game\Furniture\CatalogItem::query()
->where('catalog_name', 'like', "%{$search}%")
->orWhere('id', (int) $search)
->get(['page_id']);
$visiblePageIds = collect()
->merge($matchedPages->pluck('id'))
->merge($matchedItems->pluck('page_id'))
->filter()
->unique();
$allPages = \App\Models\Game\Furniture\CatalogPage::all(['id', 'parent_id']);
$idToParent = $allPages->pluck('parent_id', 'id');
foreach ($visiblePageIds as $pid) {
$parentId = $idToParent[$pid] ?? null;
while ($parentId && $parentId > 0) {
$visiblePageIds->push($parentId);
$parentId = $idToParent[$parentId] ?? null;
}
}
$visiblePageIds = $visiblePageIds->unique();
$rootPages = \App\Models\Game\Furniture\CatalogPage::query()
->where('parent_id', -1)
->where(function ($q) use ($visiblePageIds) {
$q->whereIn('id', $visiblePageIds)
->orWhereIn('id', function ($sub) use ($visiblePageIds) {
$sub->select('parent_id')
->from('catalog_pages')
->whereIn('id', $visiblePageIds);
});
})
->orderBy('order_num')
->get();
$expanded = $visiblePageIds->values()->all();
$this->expandedPages = array_unique(array_merge($this->expandedPages, $expanded));
if (! $this->selectedPage && $visiblePageIds->isNotEmpty()) {
$this->selectedPage = \App\Models\Game\Furniture\CatalogPage::find($visiblePageIds->first());
$this->resetTable();
}
$visibleIdsForTree = $visiblePageIds->all();
} else {
$rootPages = \App\Models\Game\Furniture\CatalogPage::query()
->where('parent_id', -1)
->orderBy('order_num')
->get();
$visibleIdsForTree = null;
}
@endphp
@include('filament.resources.hotel.catalog-editors.pages.partials.catalog-tree', [
'pages' => $rootPages,
'depth' => 0,
'selectedPage' => $selectedPage,
'visibleIds' => $visibleIdsForTree,
])
</div>
<div
x-ref="divider"
x-on:mousedown="startResize"
class="bg-gray-300 dark:bg-gray-700 hover:bg-primary-400 cursor-col-resize transition-colors duration-150 relative"
style="
width:8px;
height:100%;
border-left:1px solid rgba(0,0,0,0.05);
border-right:1px solid rgba(0,0,0,0.05);
"
>
<div class="absolute inset-y-0 left-1/2 -translate-x-1/2 w-px bg-gray-500/40"></div>
</div>
<div
class="dark:bg-gray-900 dark:border-gray-700"
style="
min-width:0;
height:100%;
overflow:hidden;
border:1px solid var(--gray-200);
border-radius:1rem;
background:var(--filament-color-white,#fff);
display:flex;
flex-direction:column;
"
>
<div style="padding:0.75rem; border-bottom:1px solid var(--gray-200);" class="dark:border-gray-700">
<div class="flex items-center justify-between gap-2">
<h2 class="font-semibold text-lg m-0">
@if($selectedPage)
Items for: <span class="text-primary-600">{{ $selectedPage->caption }}</span>
@else
Select a catalog page to view its items
@endif
</h2>
@if($selectedPage && $pageSearch === '' && $selectedPage->parent_id !== -1 && ! $this->pageHasLockedItems())
<div class="flex items-center gap-2">
<x-filament::button
wire:click="autoOrderItems"
icon="heroicon-m-arrow-path"
>
Auto Order Items
</x-filament::button>
<x-filament::button
wire:click="manualOrderItems"
icon="heroicon-m-arrow-up-on-square-stack"
color="secondary"
>
Manual Order
</x-filament::button>
</div>
@endif
</div>
@if($selectedPage && $selectedPage->parent_id === -1)
<p class="mt-2 text-xs text-gray-500">
This is a root menu entry. Select a subpage to order its items.
</p>
@elseif($selectedPage && $this->pageHasLockedItems())
<p class="mt-2 text-xs text-gray-500">
This page contains item(s) with
<code class="px-1 py-0.5 rounded bg-gray-100 dark:bg-gray-800">order_number = -1</code>.
Change or remove them to enable ordering.
</p>
@endif
</div>
<div style="flex:1 1 auto; min-height:0; overflow:auto; padding:0.75rem;">
<div style="min-width:0;">
@if($pageSearch !== '')
<div
class="mb-2 flex items-center justify-center"
x-data
>
<span class="text-[11px] px-3 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-700 shadow-sm">
Search mode active - ordering disabled
</span>
</div>
@endif
<div
data-catalog-list
data-livewire-id="{{ $this->getId() }}"
class="space-y-0"
>
{{ $this->table }}
</div>
<script>
window.catalogSelIds = @json($selectedItemIds ?? []);
window.dispatchEvent(new CustomEvent('catalog-sel-refresh'));
</script>
</div>
</div>
</div>
</div>
</x-filament-panels::page>
@@ -0,0 +1,175 @@
<ul class="pl-{{ $depth * 4 }} text-sm">
@foreach ($pages as $index => $page)
@if ($depth === 0 && $index > 0)
<li class="list-none my-2">
<div
style="
width: 100%;
height: 1px;
background-image: radial-gradient(currentColor 1px, transparent 1.5px);
background-size: 6px 1px;
color: rgba(156,163,175,0.6);
display: block;
"
class="dark:text-[rgba(107,114,128,0.7)]"
></div>
</li>
@endif
@php
$filterIds = $visibleIds ?? null;
$children = \App\Models\Game\Furniture\CatalogPage::query()
->where('parent_id', $page->id)
->when($filterIds !== null, fn ($q) => $q->whereIn('id', $filterIds))
->orderBy('order_num')
->orderBy('id')
->get();
$shouldShow = $filterIds === null
? true
: in_array($page->id, $filterIds, true) || $children->isNotEmpty();
if (! $shouldShow) {
continue;
}
$hasChildren = $children->isNotEmpty();
$iconUrl = $this->buildCatalogIconUrl((int) $page->icon_image);
$fallbackUrl = $this->buildCatalogIconUrl(1);
@endphp
<li
data-page-id="{{ $page->id }}"
class="group flex items-center gap-1 min-w-0 rounded transition-all duration-150"
{{-- Only highlight + compute drop position when dragging PAGES.
IMPORTANT: no .stop here, otherwise item drags can get blocked. --}}
@dragover.prevent="
if (!event.dataTransfer.types.includes('text/x-page-id')) return;
const rect = $el.getBoundingClientRect();
const mid = rect.top + rect.height / 2;
$el.dataset.dropPos = (event.clientY < mid) ? 'before' : 'after';
$el.classList.add('ring-2','ring-primary-400/60');
"
@dragleave.stop="
$el.classList.remove('ring-2','ring-primary-400/60');
delete $el.dataset.dropPos;
"
{{-- Page reorder drop target (keep .stop) --}}
@drop.prevent.stop="
const src = event.dataTransfer.getData('text/x-page-id');
if (src && src !== '{{ $page->id }}') {
const pos = $el.dataset.dropPos || 'after';
$wire.reorderPage(parseInt(src, 10), {{ $page->id }}, pos);
}
$el.classList.remove('ring-2','ring-primary-400/60');
delete $el.dataset.dropPos;
"
>
@if ($hasChildren)
<x-filament::icon-button
:icon="$this->isExpanded($page->id) ? 'heroicon-s-chevron-down' : 'heroicon-s-chevron-right'"
wire:click="toggleExpand({{ $page->id }})"
label="{{ $this->isExpanded($page->id) ? 'Collapse' : 'Expand' }}"
tooltip="{{ $this->isExpanded($page->id) ? 'Collapse' : 'Expand' }}"
size="xs"
color="gray"
variant="ghost"
class="shrink-0 inline-flex"
style="display:inline-flex;vertical-align:middle;"
/>
@else
<span class="inline-flex h-5 w-5 shrink-0"></span>
@endif
{{-- Page drag handle --}}
<span
x-data
draggable="true"
@dragstart="
event.dataTransfer.setData('text/x-page-id', '{{ $page->id }}');
event.dataTransfer.effectAllowed = 'move';
"
class="inline-flex h-5 w-5 shrink-0 items-center justify-center cursor-move
text-gray-400 dark:text-gray-500
opacity-0 group-hover:opacity-100 transition-opacity"
title="Drag to reorder within this level"
style="display:inline-flex;vertical-align:middle;"
>
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
<circle cx="3" cy="3" r="1.2" fill="currentColor"></circle>
<circle cx="9" cy="3" r="1.2" fill="currentColor"></circle>
<circle cx="3" cy="6" r="1.2" fill="currentColor"></circle>
<circle cx="9" cy="6" r="1.2" fill="currentColor"></circle>
<circle cx="3" cy="9" r="1.2" fill="currentColor"></circle>
<circle cx="9" cy="9" r="1.2" fill="currentColor"></circle>
</svg>
</span>
<button
x-data="{
over: false,
clickTimer: null,
clickDelay: 350,
singleClick() {
clearTimeout(this.clickTimer);
this.clickTimer = setTimeout(() => { $wire.selectPage({{ $page->id }}); }, this.clickDelay);
},
doubleClick() {
clearTimeout(this.clickTimer);
$wire.openEditPage({{ $page->id }});
},
}"
@dragover.prevent="
if (event.dataTransfer.getData('text/x-page-id')) return;
const payload = event.dataTransfer.getData('text/x-catalog-item-ids');
if (!payload) return;
over = true;"
@dragleave.prevent="over = false"
@drop.prevent.stop="
if (event.dataTransfer.getData('text/x-page-id')) return;
over = false;
const payload = event.dataTransfer.getData('text/x-catalog-item-ids');
if (!payload) return;
$wire.moveItemsToPage(payload, {{ $page->id }});"
@click.stop.prevent="singleClick()"
@dblclick.stop.prevent="doubleClick()"
class="flex-1 min-w-0 inline-flex items-center gap-0.5 px-2 py-1 rounded
hover:bg-gray-100 dark:hover:bg-gray-800 whitespace-nowrap
transition-all duration-150
{{ $selectedPage && $selectedPage->id === $page->id ? 'bg-gray-200 dark:bg-gray-700 font-semibold' : '' }}"
:class="over ? 'ring-2 ring-primary-500/50 bg-primary-50 dark:bg-primary-900/10' : ''"
title="Click to select. Double-click to edit. Drop items here to move."
style="display:inline-flex;vertical-align:middle;"
>
<span class="inline-block h-5 w-5 shrink-0"></span>
<span class="inline-flex h-5 w-5 shrink-0 items-center justify-center">
<img
src="{{ $iconUrl }}"
alt=""
class="max-w-full max-h-full object-contain align-middle"
loading="lazy"
referrerpolicy="no-referrer"
onerror="this.onerror=null;this.src='{{ $fallbackUrl }}';"
style="image-rendering: pixelated; image-rendering: crisp-edges;"
/>
</span>
<span class="truncate inline-block" style="display:inline-block;vertical-align:middle;">
{{ $page->caption }}
</span>
</button>
@if ($hasChildren && $this->isExpanded($page->id))
@include('filament.resources.hotel.catalog-editors.pages.partials.catalog-tree', [
'pages' => $children,
'depth' => $depth + 1,
'selectedPage' => $selectedPage,
'visibleIds' => $filterIds,
])
@endif
</li>
@endforeach
</ul>
@@ -0,0 +1,19 @@
<div class="my-2 text-sm tracking-tight">
@foreach($getState() as $key => $value)
<span class="inline-block p-1 mr-1 font-medium text-gray-700 whitespace-normal rounded-md dark:text-gray-200 bg-gray-500/10">
{{ $key }}
</span>
@if(is_array($value))
<span class="whitespace-normal divide-x divide-gray-200 divide-solid dark:divide-gray-700">
@foreach ($value as $nestedKey => $nestedValue)
<span class="inline-block mr-1">
{{ $nestedKey }}: {{ is_array($nestedValue) ? json_encode($nestedValue) : $nestedValue }}
</span>
@endforeach
</span>
@else
<span class="whitespace-normal">{{ $value }}</span>
@endif
@endforeach
</div>
@@ -0,0 +1,120 @@
@props([
'icon' => '',
'name' => '',
'itemId' => null,
'isSelected' => false,
'reordering' => false,
])
@php
$record = isset($getRecord) ? $getRecord() : null;
$resolvedIcon = is_callable($icon) ? $icon($record) : $icon;
$resolvedName = is_callable($name) ? $name($record) : $name;
$resolvedItemId = (int) (is_callable($itemId) ? $itemId($record) : $itemId);
@endphp
<div
x-data="{
id: {{ $resolvedItemId }},
highlight: false,
dragging: false,
compute() {
const arr = Array.isArray(window.catalogSelIds) ? window.catalogSelIds : [];
this.highlight = arr.includes(this.id);
},
dragStart(e) {
if ({{ $reordering ? 'true' : 'false' }}) return;
this.dragging = true;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/x-item-id', String(this.id));
const sel = Array.isArray(window.catalogSelIds) ? window.catalogSelIds : [];
const ids = (sel.length > 0) ? sel : [this.id];
const csv = ids
.map(v => parseInt(v, 10))
.filter(v => Number.isFinite(v) && v > 0)
.join(',');
e.dataTransfer.setData('text/x-catalog-item-ids', csv);
e.dataTransfer.setData('text/plain', csv);
e.dataTransfer.setDragImage($el, 10, 10);
},
dragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
$el.classList.add('ring-2', 'ring-primary-400/60');
},
dragLeave(e) {
$el.classList.remove('ring-2', 'ring-primary-400/60');
},
drop(e) {
e.preventDefault();
$el.classList.remove('ring-2', 'ring-primary-400/60');
const srcId = parseInt(e.dataTransfer.getData('text/x-item-id'), 10);
if (!srcId || srcId === this.id) return;
const parent = $el.closest('[data-catalog-list]');
if (!parent) return;
const children = Array.from(parent.querySelectorAll('[data-item-id]'));
const ids = children.map(c => parseInt(c.dataset.itemId, 10));
const srcIndex = ids.indexOf(srcId);
const destIndex = ids.indexOf(this.id);
if (srcIndex === -1 || destIndex === -1) return;
ids.splice(destIndex, 0, ids.splice(srcIndex, 1)[0]);
window.Livewire.find(parent.dataset.livewireId).call('reorderItems', ids);
},
clickRow(e) {
const multi = !!(e.ctrlKey || e.metaKey);
$wire.toggleSelectItem(this.id, multi);
},
openEditor() {
$wire.mountTableAction('quickEdit', this.id);
},
}"
x-init="compute(); window.addEventListener('catalog-sel-refresh', compute)"
@dragover="dragOver"
@dragleave="dragLeave"
@drop="drop"
@click.stop="clickRow"
@dblclick.stop="openEditor"
class="!flex !flex-row !items-center !gap-2 px-2 py-1 rounded select-none group cursor-default w-full"
:class="highlight ? 'bg-blue-50 dark:bg-primary-900/20 ring-1 ring-blue-400/40' : ''"
:data-item-id="id"
style="display:flex; align-items:center; gap:0.5rem;"
>
<span
x-data
draggable="true"
@dragstart="dragStart"
class="inline-flex h-5 w-5 shrink-0 items-center justify-center cursor-grab text-gray-400 dark:text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity"
title="Drag to reorder"
style="flex:0 0 auto;"
>
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
<circle cx="3" cy="3" r="1.2" fill="currentColor"></circle>
<circle cx="9" cy="3" r="1.2" fill="currentColor"></circle>
<circle cx="3" cy="6" r="1.2" fill="currentColor"></circle>
<circle cx="9" cy="6" r="1.2" fill="currentColor"></circle>
<circle cx="3" cy="9" r="1.2" fill="currentColor"></circle>
<circle cx="9" cy="9" r="1.2" fill="currentColor"></circle>
</svg>
</span>
<img
src="{{ $resolvedIcon }}"
alt=""
class="h-6 w-6 shrink-0"
loading="lazy"
draggable="false"
@dragstart.prevent
style="image-rendering: pixelated; image-rendering: crisp-edges;"
/>
<span class="truncate" draggable="false" @dragstart.prevent>{{ $resolvedName }}</span>
</div>
@@ -0,0 +1,44 @@
@props([
'itemId' => null,
'isSelected' => false,
])
@php
$record = isset($getRecord) ? $getRecord() : null;
$resolvedItemId = (int) (is_callable($itemId) ? $itemId($record) : $itemId);
$checked = (bool) (is_callable($isSelected) ? $isSelected($record) : $isSelected);
@endphp
<div
x-data="{
id: {{ $resolvedItemId }},
init() {
if (!Array.isArray(window.catalogSelIds)) window.catalogSelIds = [];
if ({{ $checked ? 'true' : 'false' }}) {
if (!window.catalogSelIds.includes(this.id)) window.catalogSelIds.push(this.id);
}
window.dispatchEvent(new CustomEvent('catalog-sel-refresh'));
},
toggle(e) {
if (!Array.isArray(window.catalogSelIds)) window.catalogSelIds = [];
if (e.target.checked) {
if (!window.catalogSelIds.includes(this.id)) window.catalogSelIds.push(this.id);
$wire.toggleSelectItem(this.id, true);
} else {
window.catalogSelIds = window.catalogSelIds.filter(x => x !== this.id);
$wire.toggleSelectItem(this.id, false);
}
window.dispatchEvent(new CustomEvent('catalog-sel-refresh'));
}
}"
x-init="init()"
class="flex items-center justify-center"
>
<input
type="checkbox"
@change="toggle($event)"
{{ $checked ? 'checked' : '' }}
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
aria-label="Select item {{ $resolvedItemId }}"
/>
</div>
@@ -0,0 +1,3 @@
<div>
<img loading="lazy" src="{{ $getBadgePath() }}" alt="{{ $getBadgeName() }}" />
</div>
@@ -0,0 +1,3 @@
<div class="pl-3" style="image-rendering: pixelated">
<img loading="lazy" src="{{ $column->getAvatarUrl() }}" alt="{{ $column->getRecord()->name }}" />
</div>
@@ -0,0 +1,5 @@
<x-filament-widgets::widget>
<x-filament::section>
{{-- Widget content --}}
</x-filament::section>
</x-filament-widgets::widget>
@@ -0,0 +1,5 @@
<x-filament-widgets::widget>
<x-filament::section>
{{-- Widget content --}}
</x-filament::section>
</x-filament-widgets::widget>
+73
View File
@@ -0,0 +1,73 @@
<x-filament-widgets::widget>
<div class="fi-section rounded-xl border-2 {{ $hasAnyUpdate ? 'border-orange-400 dark:border-orange-600' : 'border-green-400 dark:border-green-600' }} bg-white dark:bg-gray-800 shadow-sm p-5 mb-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-full {{ $hasAnyUpdate ? 'bg-orange-100 dark:bg-orange-900/50' : 'bg-green-100 dark:bg-green-900/50' }} flex items-center justify-center text-2xl">
{{ $hasAnyUpdate ? '🔄' : '✅' }}
</div>
<div class="flex-1">
@if($hasAnyUpdate)
<div class="font-bold text-lg text-gray-900 dark:text-white">Updates Beschikbaar!</div>
<div class="text-gray-500 dark:text-gray-400 text-sm mt-1">Er zijn nieuwe updates beschikbaar</div>
@else
<div class="font-bold text-lg text-gray-900 dark:text-white">Emulator Status</div>
<div class="text-gray-500 dark:text-gray-400 text-sm mt-1">Systeem is up-to-date</div>
@endif
<div class="flex flex-wrap gap-3 mt-3">
@if($emulatorUpdate)
<span class="inline-flex items-center gap-2 bg-orange-100 dark:bg-orange-900/30 rounded-lg px-3 py-1.5 text-sm text-orange-700 dark:text-orange-300 font-medium">
🖥️ <span class="font-bold">Emulator</span> v{{ $emulatorVersion }} v{{ $latestEmulatorVersion }}
</span>
@else
<span class="inline-flex items-center gap-2 bg-gray-100 dark:bg-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300 font-medium">
🖥️ <span class="font-bold">Emulator</span> v{{ $emulatorVersion }}
</span>
@endif
@if($nitroUpdate)
<span class="inline-flex items-center gap-2 bg-orange-100 dark:bg-orange-900/30 rounded-lg px-3 py-1.5 text-sm text-orange-700 dark:text-orange-300 font-medium">
🎮 <span class="font-bold">Client</span> v{{ $nitroVersion }} v{{ $latestNitroVersion }}
</span>
@else
<span class="inline-flex items-center gap-2 bg-gray-100 dark:bg-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300 font-medium">
🎮 <span class="font-bold">Client</span> v{{ $nitroVersion ?? 'Niet ingesteld' }}
</span>
@endif
<span class="inline-flex items-center gap-2 bg-gray-100 dark:bg-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300 font-medium">
👥 {{ $onlineUsers }} online
</span>
<span class="inline-flex items-center gap-2 bg-gray-100 dark:bg-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300 font-medium">
💾 {{ $dbSize }} DB
</span>
<span class="inline-flex items-center gap-2 {{ $sqlPending > 0 ? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300' }} rounded-lg px-3 py-1.5 text-sm font-medium">
📊 SQL: {{ $sqlApplied }} toegepast{{ $sqlPending > 0 ? ', ' . $sqlPending . ' pending' : '' }}
</span>
</div>
</div>
</div>
<div class="flex items-center gap-3 ml-4">
<a href="/housekeeping/alert-settings" class="inline-flex items-center justify-center rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 transition">
⚙️ Instellingen
</a>
<button wire:click="diagnoseSystem" wire:loading.attr="disabled" class="inline-flex items-center justify-center rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 transition disabled:opacity-50">
<span wire:loading.remove wire:target="diagnoseSystem">🔍 Diagnose</span>
<span wire:loading wire:target="diagnoseSystem">...</span>
</button>
<button wire:click="repairSystem" wire:loading.attr="disabled" wire:confirm="Systeem repareren? Dit kan eventuele problemen oplossen." class="inline-flex items-center justify-center rounded-lg border border-yellow-500 dark:border-yellow-600 bg-yellow-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-yellow-600 transition disabled:opacity-50">
<span wire:loading.remove wire:target="repairSystem">🔧 Reparerer</span>
<span wire:loading wire:target="repairSystem">...</span>
</button>
@if($hasAnyUpdate)
<button wire:click="updateAll" wire:loading.attr="disabled" wire:confirm="Alle updates nu installeren?" class="inline-flex items-center justify-center rounded-lg bg-orange-500 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-orange-600 transition disabled:opacity-50">
<span wire:loading.remove wire:target="updateAll">🔄 Alles Updaten</span>
<span wire:loading wire:target="updateAll"> Bezig...</span>
</button>
@else
<button wire:click="forceCheck" wire:loading.attr="disabled" class="inline-flex items-center justify-center rounded-lg bg-blue-500 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-600 transition disabled:opacity-50">
<span wire:loading.remove wire:target="forceCheck">🔍 Check Nu</span>
<span wire:loading wire:target="forceCheck"> Bezig...</span>
</button>
@endif
</div>
</div>
</div>
</x-filament-widgets::widget>
+70
View File
@@ -0,0 +1,70 @@
<x-installation-layout>
<x-content.installation-content-section icon="hotel-icon" classes="border">
<x-slot:title>
{{ __('Welcome to Atom CMS') }}
</x-slot:title>
<x-slot:under-title>
{{ __('We are delighted of having you trying Atom CMS') }}
</x-slot:under-title>
<div class="space-y-3">
<p>
{{ __('Hello there! We are truly grateful that you have chosen Atom CMS for your hotel.') }}
</p>
<p>
{{ __('Atom CMS is built with the community in mind, meaning we highly value community input, rather than only bringing our own ideas & vision to the CMS we try our very best to implement suggestions made by our beloved community. We want everyone to be able to contribute or customise Atom CMS to their needs without having a bachelor in programming.') }}
</p>
<p>{{ __('Atom CMS sole purpose is to empower hotel owners like you. We want you to be able to run your hotel with ease. Our user-friendly interface, robust features, and helpful community are here to ensure that your experience with Atom CMS is nothing short of exceptional!') }}</p>
<p>
{{ __('As you dive into Atom CMS, we encourage you to explore the extensive range of features we have curated to help you bring your vision to life. From customizable templates to seamless integrations with clients like Nitro, we will have you set up in no time.') }}
</p>
<p>
{!! __('To get started, we recommend checking out our <a href=":documentation_link" target="_blank" class="font-semibold underline">comprehensive documentation</a>, which will guide you through the initial setup and help you make the most of your Atom CMS experience.', ['documentation_link' => 'https://github.com/atom-retros/atomcms/wiki']) !!}
</p>
<p>
{!! __('And remember, please <a href=":discord_link" target="_blank" class="font-semibold underline">join our Discord</a>. If you have any questions or need assistance, please do not hesitate to reach out. We are here to help you every step of the way.', ['discord_link' => 'https://discord.gg/pP6HyZedAj']) !!}
</p>
<p>
{{ __('Once again, thank you for choosing Atom CMS, and we cannot wait to see the incredible project you will create.') }}
</p>
<p class="font-semibold italic">
{{ __('With everything being said we just want to wish you a warm welcome to the Atom CMS family!') }}
</p>
<hr>
<div>
{{ __('To avoid any third-party party abuse, please provide the installation code, which can be found in your database inside the "website_installation" table under the column "installation_key".') }}
</div>
<form action="{{ route('installation.start-installation') }}" method="POST">
@csrf
<label class="block font-semibold text-gray-700" for="installation_key">
{{ __('Installation key') }}
</label>
<input
class="focus:ring-0 border-4 border-gray-200 rounded focus:border-[#eeb425] w-full @error('installation_key')border-red-600 ring-red-500 @enderror"
id="installation_key" type="text" name="installation_key" placeholder="{{ __('Enter your installation key') }}" autofocus required autocomplete="false">
@error('installation_key')
<p class="mt-1 text-xs italic text-red-500">
{{ $message }}
</p>
@enderror
<x-form.secondary-button classes="mt-3">
{{ __('Start the setup') }}
</x-form.secondary-button>
</form>
</div>
</x-content.installation-content-section>
</x-installation-layout>
+49
View File
@@ -0,0 +1,49 @@
<x-installation-layout>
<x-content.installation-content-section icon="hotel-icon" classes="border">
<x-slot:title>
{{ __('Welcome to Atom CMS') }}
</x-slot:title>
<x-slot:under-title>
{{ __('We are delighted of having you trying Atom CMS') }}
</x-slot:under-title>
<form action="{{ route('installation.save-step') }}" method="POST" class="space-y-3">
@csrf
@foreach($settings as $setting)
<div>
<label class="block font-semibold text-gray-700" for="{{ $setting->key }}">
{{ Str::replace('_', ' ', Str::ucfirst($setting->key)) }}
</label>
<input
class="focus:ring-0 border-4 border-gray-200 rounded focus:border-[#eeb425] w-full @error($setting->key)border-red-600 ring-red-500 @enderror"
id="{{ $setting->key }}" type="text" name="{{ $setting->key }}" value="{{ $setting->value }}" placeholder="{{ $setting->key }}" required>
@error($setting->key)
<p class="mt-1 text-xs italic text-red-500">
{{ $message }}
</p>
@enderror
<small>{{ $setting->comment }}</small>
</div>
@endforeach
<x-form.secondary-button>
{{ __('Continue to step 2') }}
</x-form.secondary-button>
</form>
<div class="flex gap-x-4">
<form action="{{ route('installation.restart') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.danger-button>
{{ __('Restart installation') }}
</x-form.danger-button>
</form>
</div>
</x-content.installation-content-section>
</x-installation-layout>
+57
View File
@@ -0,0 +1,57 @@
<x-installation-layout>
<x-content.installation-content-section icon="hotel-icon" classes="border">
<x-slot:title>
{{ __('Welcome to Atom CMS') }}
</x-slot:title>
<x-slot:under-title>
{{ __('We are delighted of having you trying Atom CMS') }}
</x-slot:under-title>
<form action="{{ route('installation.save-step') }}" method="POST" class="space-y-3">
@csrf
@foreach($settings as $setting)
<div>
<label class="block font-semibold text-gray-700" for="{{ $setting->key }}">
{{ Str::replace('_', ' ', Str::ucfirst($setting->key)) }}
</label>
<input
class="focus:ring-0 border-4 border-gray-200 rounded focus:border-[#eeb425] w-full @error($setting->key)border-red-600 ring-red-500 @enderror"
id="{{ $setting->key }}" type="text" name="{{ $setting->key }}" value="{{ $setting->value }}" placeholder="{{ $setting->key }}" required>
@error($setting->key)
<p class="mt-1 text-xs italic text-red-500">
{{ $message }}
</p>
@enderror
<small>{{ $setting->comment }}</small>
</div>
@endforeach
<x-form.secondary-button>
{{ __('Continue to step 3') }}
</x-form.secondary-button>
</form>
<div class="flex gap-x-4">
<form action="{{ route('installation.previous-step') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.primary-button>
{{ __('Previous step') }}
</x-form.primary-button>
</form>
<form action="{{ route('installation.restart') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.danger-button>
{{ __('Restart installation') }}
</x-form.danger-button>
</form>
</div>
</x-content.installation-content-section>
</x-installation-layout>
+57
View File
@@ -0,0 +1,57 @@
<x-installation-layout>
<x-content.installation-content-section icon="hotel-icon" classes="border">
<x-slot:title>
{{ __('Welcome to Atom CMS') }}
</x-slot:title>
<x-slot:under-title>
{{ __('We are delighted of having you trying Atom CMS') }}
</x-slot:under-title>
<form action="{{ route('installation.save-step') }}" method="POST" class="space-y-3">
@csrf
@foreach($settings as $setting)
<div>
<label class="block font-semibold text-gray-700" for="{{ $setting->key }}">
{{ Str::replace('_', ' ', Str::ucfirst($setting->key)) }}
</label>
<input
class="focus:ring-0 border-4 border-gray-200 rounded focus:border-[#eeb425] w-full @error($setting->key)border-red-600 ring-red-500 @enderror"
id="{{ $setting->key }}" type="text" name="{{ $setting->key }}" value="{{ $setting->value }}" placeholder="{{ $setting->key }}" required>
@error($setting->key)
<p class="mt-1 text-xs italic text-red-500">
{{ $message }}
</p>
@enderror
<small>{{ $setting->comment }}</small>
</div>
@endforeach
<x-form.secondary-button>
{{ __('Continue to step 4') }}
</x-form.secondary-button>
</form>
<div class="flex gap-x-4">
<form action="{{ route('installation.previous-step') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.primary-button>
{{ __('Previous step') }}
</x-form.primary-button>
</form>
<form action="{{ route('installation.restart') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.danger-button>
{{ __('Restart installation') }}
</x-form.danger-button>
</form>
</div>
</x-content.installation-content-section>
</x-installation-layout>
+57
View File
@@ -0,0 +1,57 @@
<x-installation-layout>
<x-content.installation-content-section icon="hotel-icon" classes="border">
<x-slot:title>
{{ __('Welcome to Atom CMS') }}
</x-slot:title>
<x-slot:under-title>
{{ __('We are delighted of having you trying Atom CMS') }}
</x-slot:under-title>
<form action="{{ route('installation.save-step') }}" method="POST" class="space-y-3">
@csrf
@foreach($settings as $setting)
<div>
<label class="block font-semibold text-gray-700" for="{{ $setting->key }}">
{{ Str::replace('_', ' ', Str::ucfirst($setting->key)) }}
</label>
<input
class="focus:ring-0 border-4 border-gray-200 rounded focus:border-[#eeb425] w-full @error($setting->key)border-red-600 ring-red-500 @enderror"
id="{{ $setting->key }}" type="text" name="{{ $setting->key }}" value="{{ $setting->value }}" placeholder="{{ $setting->key }}">
@error($setting->key)
<p class="mt-1 text-xs italic text-red-500">
{{ $message }}
</p>
@enderror
<small>{{ $setting->comment }}</small>
</div>
@endforeach
<x-form.secondary-button>
{{ __('Complete setup') }}
</x-form.secondary-button>
</form>
<div class="flex gap-x-4">
<form action="{{ route('installation.previous-step') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.primary-button>
{{ __('Previous step') }}
</x-form.primary-button>
</form>
<form action="{{ route('installation.restart') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.danger-button>
{{ __('Restart installation') }}
</x-form.danger-button>
</form>
</div>
</x-content.installation-content-section>
</x-installation-layout>
+86
View File
@@ -0,0 +1,86 @@
<x-installation-layout>
<x-content.installation-content-section icon="hotel-icon" classes="border">
<x-slot:title>
{{ __('Choose Your Style') }}
</x-slot:title>
<x-slot:under-title>
{{ __('Pick a preset style for your hotel') }}
</x-slot:under-title>
<form action="{{ route('installation.complete') }}" method="POST" class="space-y-6">
@csrf
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
@php
$presets = [
['id' => 'default', 'name' => 'Default', 'color' => '#eeb425', 'desc' => 'Classic gold theme'],
['id' => 'atom_original', 'name' => 'Atom Original', 'color' => '#eeb425', 'desc' => 'Light fresh look'],
['id' => 'modern', 'name' => 'Modern', 'color' => '#6366f1', 'desc' => 'Sleek purple'],
['id' => 'retro', 'name' => 'Retro', 'color' => '#f43f5e', 'desc' => 'Retro gaming vibes'],
['id' => 'nature', 'name' => 'Nature', 'color' => '#22c55e', 'desc' => 'Fresh green theme'],
['id' => 'ocean', 'name' => 'Ocean', 'color' => '#0ea5e9', 'desc' => 'Deep blue waters'],
['id' => 'sunset', 'name' => 'Sunset', 'color' => '#f97316', 'desc' => 'Warm orange glow'],
['id' => 'royal', 'name' => 'Royal', 'color' => '#8b5cf6', 'desc' => 'Elegant purple'],
['id' => 'minimal', 'name' => 'Minimal', 'color' => '#000000', 'desc' => 'Clean black & white'],
['id' => 'cyberpunk', 'name' => 'Cyberpunk', 'color' => '#ff00ff', 'desc' => 'Neon sci-fi'],
['id' => 'christmas', 'name' => 'Christmas', 'color' => '#dc2626', 'desc' => 'Festive red/green'],
['id' => 'halloween', 'name' => 'Halloween', 'color' => '#f97316', 'desc' => 'Spooky orange/purple'],
['id' => 'valentine', 'name' => 'Valentine', 'color' => '#ec4899', 'desc' => 'Romantic pink'],
['id' => 'matrix', 'name' => 'Matrix', 'color' => '#00ff00', 'desc' => 'Digital green'],
['id' => 'discord', 'name' => 'Discord', 'color' => '#5865f2', 'desc' => 'Discord-inspired'],
['id' => 'neon', 'name' => 'Neon', 'color' => '#ff00ff', 'desc' => 'Glowing neon'],
['id' => 'coffee', 'name' => 'Coffee', 'color' => '#8b5a2b', 'desc' => 'Warm brown tones'],
['id' => 'strawberry', 'name' => 'Strawberry', 'color' => '#ff6b6b', 'desc' => 'Sweet pink'],
['id' => 'lavender', 'name' => 'Lavender', 'color' => '#a855f7', 'desc' => 'Calm purple'],
['id' => 'midnight', 'name' => 'Midnight', 'color' => '#60a5fa', 'desc' => 'Deep dark blue'],
['id' => 'golden', 'name' => 'Golden', 'color' => '#fbbf24', 'desc' => 'Luxury gold'],
['id' => 'arctic', 'name' => 'Arctic', 'color' => '#06b6d4', 'desc' => 'Icy cyan'],
['id' => 'magma', 'name' => 'Magma', 'color' => '#ef4444', 'desc' => 'Hot red/lava'],
['id' => 'forest', 'name' => 'Forest', 'color' => '#16a34a', 'desc' => 'Deep green woods'],
];
$selectedPreset = setting('preset', 'default');
@endphp
@foreach($presets as $preset)
<label class="cursor-pointer group">
<input type="radio" name="selected_preset" value="{{ $preset['id'] }}" class="peer sr-only" {{ $selectedPreset === $preset['id'] ? 'checked' : '' }}>
<div class="p-4 rounded-xl border-2 border-gray-200 peer-checked:border-[{{ $preset['color'] }}] peer-checked:shadow-lg transition-all duration-200 hover:border-gray-300 hover:shadow-md">
<div class="flex items-center gap-3 mb-2">
<div class="w-8 h-8 rounded-lg" style="background-color: {{ $preset['color'] }}"></div>
<span class="font-semibold text-gray-800">{{ $preset['name'] }}</span>
</div>
<p class="text-xs text-gray-500">{{ $preset['desc'] }}</p>
</div>
</label>
@endforeach
</div>
<input type="hidden" name="theme" value="{{ setting('theme', 'atom') }}">
<input type="hidden" name="preset" value="custom">
<x-form.secondary-button>
{{ __('Save Style & Continue') }}
</x-form.secondary-button>
</form>
<div class="flex gap-x-4">
<form action="{{ route('installation.previous-step') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.primary-button>
{{ __('Previous step') }}
</x-form.primary-button>
</form>
<form action="{{ route('installation.restart') }}" method="POST" class="w-full mt-3">
@csrf
<x-form.danger-button>
{{ __('Restart installation') }}
</x-form.danger-button>
</form>
</div>
</x-content.installation-content-section>
</x-installation-layout>
+26
View File
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="turbolinks-cache-control" content="no-cache">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ setting('hotel_name') }} - @stack('title')</title>
<link rel="icon" type="image/gif" sizes="18x17" href="{{ asset('assets/images/home_icon.gif') }}">
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">
@vite(['resources/css/global.css', 'resources/js/global.js'], 'build')
@stack('scripts')
</head>
<body class="flex justify-center items-center min-h-screen site-bg">
<main class="mx-auto w-full lg:w-[40%] p-4 lg:px-0 lg:py-4">
{{ $slot }}
</main>
</body>
</html>
+59
View File
@@ -0,0 +1,59 @@
<div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
{{-- Header --}}
<div class="flex flex-wrap items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 gap-3">
<div class="flex items-center gap-3">
<h3 class="text-lg font-semibold">📜 Emulator Logs</h3>
<select wire:model="selectedFile" wire:change="loadLogContent" class="text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-lg px-3 py-1.5">
@foreach($logFiles as $file)
<option value="{{ $file }}">{{ basename($file) }}</option>
@endforeach
</select>
<select wire:model="lines" wire:change="loadLogContent" class="text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-lg px-3 py-1.5">
<option value="50">50 regels</option>
<option value="100">100 regels</option>
<option value="200">200 regels</option>
<option value="500">500 regels</option>
<option value="1000">1000 regels</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<input type="checkbox" wire:model="autoRefresh" class="rounded">
Auto-refresh ({{ $refreshInterval }}s)
</label>
<button wire:click="loadLogContent" class="px-3 py-1.5 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
🔄 Vernieuw
</button>
<button wire:click="clearLog" onclick="return confirm('Weet je zeker dat je het log wilt legen?')" class="px-3 py-1.5 text-sm bg-red-500 text-white rounded-lg hover:bg-red-600 transition">
🗑️ Leeg
</button>
</div>
</div>
{{-- Log Content --}}
<div class="p-4">
<div class="bg-gray-900 rounded-lg overflow-hidden">
<div class="flex items-center justify-between px-4 py-2 bg-gray-800 border-b border-gray-700">
<span class="text-xs text-gray-400">{{ basename($selectedFile) ?? 'Geen bestand' }}</span>
<span class="text-xs text-gray-500">{{ $lines }} regels</span>
</div>
<pre class="p-4 text-green-400 text-xs font-mono overflow-auto max-h-96 whitespace-pre-wrap leading-relaxed">{{ $logContent }}</pre>
</div>
</div>
{{-- Auto Refresh Script --}}
@if($autoRefresh)
<script>
setInterval(() => {
@this.call('loadLogContent');
}, {{ $refreshInterval * 1000 }});
</script>
@endif
</div>
</div>
@@ -0,0 +1,5 @@
<div>
{{ $this->toggleStatusAction }}
<x-filament-actions::modals />
</div>
+8
View File
@@ -0,0 +1,8 @@
<div class="flex items-center gap-2">
{{ $this->toggleMaintenanceAction }}
{{ $this->scheduleMaintenanceAction }}
{{ $this->previewMaintenanceAction }}
{{ $this->editMaintenanceMessageAction }}
<x-filament-actions::modals />
</div>