You've already forked Atomcms-edit
Initial commit
This commit is contained in:
@@ -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>
|
||||
+57
@@ -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>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
|
||||
<div>
|
||||
|
||||
{{ $getModifiedState() ?? (!is_array($getState()) ? $getState() ?? $getPlaceholder() : null) }}
|
||||
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
+53
@@ -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>
|
||||
+13
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
Executable
+277
@@ -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>
|
||||
Executable
+175
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user