Refactor: Pagination mit Livewire WithPagination + LengthAwarePaginator

- WithPagination Trait + LengthAwarePaginator für Array-Daten
- $messages->links() statt manueller Pagination-Blöcke
- Livewire tailwind.blade.php überschrieben mit mq-pagination/mq-pag-btn Klassen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main v1.1.144
boban 2026-04-23 02:09:58 +02:00
parent bc66870681
commit 8699b9d991
8 changed files with 293 additions and 59 deletions

View File

@ -2,33 +2,34 @@
namespace App\Livewire\Ui\Nx\Mail;
use Illuminate\Pagination\LengthAwarePaginator;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.dvx')]
#[Title('Quarantäne · Mailwolt')]
class QuarantineList extends Component
{
use WithPagination;
#[Url(as: 'filter', keep: true)]
public string $filter = 'suspicious';
#[Url(as: 'q', keep: true)]
public string $search = '';
#[Url(as: 'page', keep: true)]
public int $page = 1;
public int $perPage = 25;
public int $rows = 500;
#[On('quarantine:updated')]
public function refresh(): void {}
public function updatedFilter(): void { $this->page = 1; }
public function updatedSearch(): void { $this->page = 1; }
public function updatedFilter(): void { $this->resetPage(); }
public function updatedSearch(): void { $this->resetPage(); }
public function openMessage(string $msgId): void
{
@ -68,15 +69,18 @@ class QuarantineList extends Component
}
$total = count($messages);
$totalPages = max(1, (int) ceil($total / $this->perPage));
$this->page = max(1, min($this->page, $totalPages));
$paged = array_slice($messages, ($this->page - 1) * $this->perPage, $this->perPage);
$currentPage = LengthAwarePaginator::resolveCurrentPage();
$paged = new LengthAwarePaginator(
array_slice($messages, ($currentPage - 1) * $this->perPage, $this->perPage),
$total,
$this->perPage,
$currentPage,
['path' => request()->url()]
);
return view('livewire.ui.nx.mail.quarantine-list', [
'messages' => $paged,
'counts' => $counts,
'total' => $total,
'totalPages' => $totalPages,
]);
}

View File

@ -2,25 +2,26 @@
namespace App\Livewire\Ui\Nx\Mail;
use Illuminate\Pagination\LengthAwarePaginator;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.dvx')]
#[Title('Mail-Queue · Mailwolt')]
class QueueList extends Component
{
use WithPagination;
#[Url(as: 'filter', keep: true)]
public string $filter = 'all';
#[Url(as: 'q', keep: true)]
public string $search = '';
#[Url(as: 'page', keep: true)]
public int $page = 1;
public int $perPage = 25;
public array $selected = [];
public bool $selectAll = false;
@ -28,8 +29,8 @@ class QueueList extends Component
#[On('queue:updated')]
public function refresh(): void {}
public function updatedFilter(): void { $this->page = 1; $this->selected = []; $this->selectAll = false; }
public function updatedSearch(): void { $this->page = 1; }
public function updatedFilter(): void { $this->resetPage(); $this->selected = []; $this->selectAll = false; }
public function updatedSearch(): void { $this->resetPage(); }
public function updatedSelectAll(bool $val): void
{
@ -97,15 +98,18 @@ class QueueList extends Component
}
$total = count($messages);
$totalPages = max(1, (int) ceil($total / $this->perPage));
$this->page = max(1, min($this->page, $totalPages));
$paged = array_slice($messages, ($this->page - 1) * $this->perPage, $this->perPage);
$currentPage = LengthAwarePaginator::resolveCurrentPage();
$paged = new LengthAwarePaginator(
array_slice($messages, ($currentPage - 1) * $this->perPage, $this->perPage),
$total,
$this->perPage,
$currentPage,
['path' => request()->url()]
);
return view('livewire.ui.nx.mail.queue-list', [
'messages' => $paged,
'counts' => $counts,
'total' => $total,
'totalPages' => $totalPages,
]);
}

View File

@ -8,7 +8,7 @@
<svg width="16" height="16" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M7 4v4l2.5 2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Quarantäne
<span class="mq-total-badge">{{ $counts['all'] }}</span>
<span class="mq-page-sub">Rspamd · {{ $total }} Einträge</span>
<span class="mq-page-sub">Rspamd · {{ $messages->total() }} Einträge</span>
</div>
<div class="mq-page-actions">
<div class="mq-search-wrap">
@ -99,22 +99,7 @@
</tbody>
</table>
</div>
@if($totalPages > 1)
<div class="mq-pagination">
<span class="mq-pag-info">{{ ($page - 1) * $perPage + 1 }}{{ min($page * $perPage, $total) }} von {{ $total }}</span>
<div class="mq-pag-btns">
<button wire:click="$set('page', {{ max(1, $page - 1) }})" class="mq-pag-btn" @if($page <= 1) disabled @endif>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 2L3.5 6l4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@for($i = max(1, $page - 2); $i <= min($totalPages, $page + 2); $i++)
<button wire:click="$set('page', {{ $i }})" class="mq-pag-btn {{ $i === $page ? 'active' : '' }}">{{ $i }}</button>
@endfor
<button wire:click="$set('page', {{ min($totalPages, $page + 1) }})" class="mq-pag-btn" @if($page >= $totalPages) disabled @endif>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 2L8.5 6l-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</div>
@endif
{{ $messages->links() }}
@elseif($counts['all'] === 0)
<div class="mq-empty">
<svg width="36" height="36" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1" opacity=".3"/><path d="M7 4v4l2.5 2" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity=".3"/></svg>

View File

@ -92,22 +92,7 @@
</tbody>
</table>
</div>
@if($totalPages > 1)
<div class="mq-pagination">
<span class="mq-pag-info">{{ ($page - 1) * $perPage + 1 }}{{ min($page * $perPage, $total) }} von {{ $total }}</span>
<div class="mq-pag-btns">
<button wire:click="$set('page', {{ max(1, $page - 1) }})" class="mq-pag-btn" @if($page <= 1) disabled @endif>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 2L3.5 6l4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@for($i = max(1, $page - 2); $i <= min($totalPages, $page + 2); $i++)
<button wire:click="$set('page', {{ $i }})" class="mq-pag-btn {{ $i === $page ? 'active' : '' }}">{{ $i }}</button>
@endfor
<button wire:click="$set('page', {{ min($totalPages, $page + 1) }})" class="mq-pag-btn" @if($page >= $totalPages) disabled @endif>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 2L8.5 6l-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</div>
@endif
{{ $messages->links() }}
@else
<div class="mq-empty">
<svg width="36" height="36" viewBox="0 0 14 14" fill="none"><path d="M1 4h12M4 4V2h6v2M4 12V7m6 5V7" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity=".3"/></svg>

View File

@ -0,0 +1,102 @@
@php
if (! isset($scrollTo)) {
$scrollTo = 'body';
}
$scrollIntoViewJsSnippet = ($scrollTo !== false)
? <<<JS
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
JS
: '';
@endphp
<div>
@if ($paginator->hasPages())
<nav class="d-flex justify-items-center justify-content-between">
<div class="d-flex justify-content-between flex-fill d-sm-none">
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.previous')</span>
</li>
@else
<li class="page-item">
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
</li>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
</li>
@else
<li class="page-item disabled" aria-disabled="true">
<span class="page-link" aria-hidden="true">@lang('pagination.next')</span>
</li>
@endif
</ul>
</div>
<div class="d-none flex-sm-fill d-sm-flex align-items-sm-center justify-content-sm-between">
<div>
<p class="small text-muted">
{!! __('Showing') !!}
<span class="fw-semibold">{{ $paginator->firstItem() }}</span>
{!! __('to') !!}
<span class="fw-semibold">{{ $paginator->lastItem() }}</span>
{!! __('of') !!}
<span class="fw-semibold">{{ $paginator->total() }}</span>
{!! __('results') !!}
</p>
</div>
<div>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
@else
<li class="page-item">
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" aria-label="@lang('pagination.previous')">&lsaquo;</button>
</li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="page-item active" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}" aria-current="page"><span class="page-link">{{ $page }}</span></li>
@else
<li class="page-item" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}"><button type="button" class="page-link" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}">{{ $page }}</button></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" aria-label="@lang('pagination.next')">&rsaquo;</button>
</li>
@else
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
@endif
</ul>
</div>
</div>
</nav>
@endif
</div>

View File

@ -0,0 +1,53 @@
@php
if (! isset($scrollTo)) {
$scrollTo = 'body';
}
$scrollIntoViewJsSnippet = ($scrollTo !== false)
? <<<JS
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
JS
: '';
@endphp
<div>
@if ($paginator->hasPages())
<nav>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.previous')</span>
</li>
@else
@if(method_exists($paginator,'getCursorName'))
<li class="page-item">
<button dusk="previousPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
</li>
@else
<li class="page-item">
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
</li>
@endif
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
@if(method_exists($paginator,'getCursorName'))
<li class="page-item">
<button dusk="nextPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
</li>
@else
<li class="page-item">
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
</li>
@endif
@else
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.next')</span>
</li>
@endif
</ul>
</nav>
@endif
</div>

View File

@ -0,0 +1,56 @@
@php
if (! isset($scrollTo)) {
$scrollTo = 'body';
}
$scrollIntoViewJsSnippet = ($scrollTo !== false)
? <<<JS
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
JS
: '';
@endphp
<div>
@if ($paginator->hasPages())
<nav role="navigation" aria-label="Pagination Navigation" class="flex justify-between">
<span>
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
{!! __('pagination.previous') !!}
</span>
@else
@if(method_exists($paginator,'getCursorName'))
<button type="button" dusk="previousPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
{!! __('pagination.previous') !!}
</button>
@else
<button
type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
{!! __('pagination.previous') !!}
</button>
@endif
@endif
</span>
<span>
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
@if(method_exists($paginator,'getCursorName'))
<button type="button" dusk="nextPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
{!! __('pagination.next') !!}
</button>
@else
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
{!! __('pagination.next') !!}
</button>
@endif
@else
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
{!! __('pagination.next') !!}
</span>
@endif
</span>
</nav>
@endif
</div>

View File

@ -0,0 +1,45 @@
@if ($paginator->hasPages())
<div class="mq-pagination">
<span class="mq-pag-info">
{{ $paginator->firstItem() }}{{ $paginator->lastItem() }} von {{ $paginator->total() }}
</span>
<div class="mq-pag-btns">
@if ($paginator->onFirstPage())
<button class="mq-pag-btn" disabled>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 2L3.5 6l4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@else
<button wire:click="previousPage" wire:loading.attr="disabled" class="mq-pag-btn">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 2L3.5 6l4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@endif
@foreach ($elements as $element)
@if (is_string($element))
<button class="mq-pag-btn" disabled></button>
@endif
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<button class="mq-pag-btn active">{{ $page }}</button>
@else
<button wire:click="gotoPage({{ $page }})" class="mq-pag-btn">{{ $page }}</button>
@endif
@endforeach
@endif
@endforeach
@if ($paginator->hasMorePages())
<button wire:click="nextPage" wire:loading.attr="disabled" class="mq-pag-btn">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 2L8.5 6l-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@else
<button class="mq-pag-btn" disabled>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 2L8.5 6l-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@endif
</div>
</div>
@endif