Files
SealShare/resources/views/livewire/admin/admin-dashboard.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 5853f1f7d6 Give every page the share pages' layout, at one width
The pages were built five ways: two layouts, seven widths from 28 to
64rem and four heading styles. Every page now looks like the share
pages: a centred heading over one 40rem column of outlined cards, with
the floating toolbar below.

- <x-page> (components/page.blade.php) is the root of every page. It
  draws the h1 and its line (`brand` takes the site's logo, title and
  description from Admin settings), an optional `mark` and `navigation`
  slot, then the content. It has no width prop: every page is the same
  <x-pane width="narrow">.
- The sign-in, password reset, confirm, verify email, two-factor
  challenge, setup and system password pages move onto layouts/app with
  the brand heading and their form in a card titled with the task.
  layouts/auth, auth-header and the settings heading partial are gone,
  and so is the per-page width CSS.
- Settings put their section nav under the heading; the admin pages get
  a description line each. FileUploader and ShareDownload no longer pass
  the branding to their views.
- The admin dashboard's table needed about 49rem, so its shares are a
  list: created above the token, which opens the share, then files,
  size, downloads and expiry on two lines that wrap instead of clipping,
  and one delete button. A "Sort by" select replaces the column headers
  (newest, oldest, expiring soonest with never-expiring last, largest,
  most downloads, most files) and resets the page. The stats stay two
  by two. table.css and sort-header.css are no longer imported.
- Branding hints in Admin settings name every page the title shows on.
- Tests: PageTemplateTest renders every page once and checks one page
  template, one h1 and the width, and the brand heading with its
  fallbacks. FrameTest measures the page column instead of the auth
  card and the 64rem main; dashboard tests follow the list and the sort
  select, including expiry order. .ai/rules/views.md records <x-page>,
  the CHANGELOG notes the change and the website screenshots are
  regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 21:42:04 +02:00

74 lines
4.5 KiB
PHP

<x-page :title="__('Admin Dashboard')" :description="__('Shares, files and storage at a glance')">
<x-grid :columns="2" gap="space200">
<x-stat :title="__('Total Shares')" :value="$totalShares" icon="link" />
<x-stat :title="__('Active Shares')" :value="$activeShares" icon="schedule" />
<x-stat :title="__('Total Files')" :value="$totalFiles" icon="description" />
<x-stat :title="__('Disk Usage')" :value="Number::fileSize($usedSpace)" icon="hard_drive" :description="Number::fileSize($usedSpace).' / '.Number::fileSize($maxQuota)">
<x-progress :value="$maxQuota > 0 ? min(100, ($usedSpace / $maxQuota) * 100) : 0" :label="__('Disk Usage')" />
</x-stat>
</x-grid>
{{-- The shares as a list, not a table: a table's columns need more than the page's 40rem, and
every page keeps that one width. The sort is a select above the list instead of column headers. --}}
<x-card :title="__('All Shares')" heading="h2" variant="outlined">
<x-stack gap="space200">
@if ($shares->total() === 0)
<x-empty-state icon="link_off" :title="__('No shares yet')" :description="__('Shares appear here once someone uploads files.')" />
@else
<div class="admin-shares-sort">
<x-select
wire:model.live="sort"
:label="__('Sort by')"
:options="[
['id' => 'newest', 'name' => __('Newest first')],
['id' => 'oldest', 'name' => __('Oldest first')],
['id' => 'expiring', 'name' => __('Expiring soonest')],
['id' => 'largest', 'name' => __('Largest')],
['id' => 'most-downloaded', 'name' => __('Most downloads')],
['id' => 'most-files', 'name' => __('Most files')],
]"
data-test="shares-sort"
/>
</div>
{{-- Each share fits the column on a phone: the token opens it, so delete is the one button;
the details are two short lines that never clip (admin-shares, app.css). --}}
<x-list dividers :label="__('All Shares')" class="admin-shares">
@foreach ($shares as $share)
<x-list-item :overline="__('Created :time', ['time' => $share->created_at->diffForHumans()])" wire:key="share-{{ $share->id }}" data-test="share-row">
<a href="{{ route('share.download', $share) }}" target="_blank" rel="noopener" class="md-link"><code>{{ $share->token }}</code></a>
<x-slot:description>
<span class="admin-share-detail md-tabular">{{ trans_choice(':count file|:count files', $share->files_count) }} · {{ Number::fileSize($share->total_size) }} · {{ trans_choice(':count download|:count downloads', $share->download_count) }}</span>
@if (! $share->expires_at)
<span class="admin-share-detail">{{ __('Never expires') }}</span>
@elseif ($share->isExpired())
<span class="admin-share-detail md-ink-error">{{ __('Expired :time', ['time' => $share->expires_at->diffForHumans()]) }}</span>
@else
<span class="admin-share-detail">{{ __('Expires :time', ['time' => $share->expires_at->diffForHumans()]) }}</span>
@endif
</x-slot:description>
<x-slot:end>
<x-button icon="delete" :tooltip="__('Delete')" color="error" wire:click="$set('deletingShareId', {{ $share->id }})" data-test="delete-share-{{ $share->id }}" />
</x-slot:end>
</x-list-item>
@endforeach
</x-list>
{{ $shares->links() }}
@endif
</x-stack>
</x-card>
<x-modal wire:model="deletingShareId" :title="__('Delete this share?')" icon="delete">
{{ __('Are you sure you want to delete this share?') }}
<x-slot:actions>
<x-button :label="__('Cancel')" x-on:click="close()" />
<x-button :label="__('Delete')" danger x-on:click="$wire.deleteShare($wire.deletingShareId)" data-test="confirm-delete-share" />
</x-slot:actions>
</x-modal>
</x-page>