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>
165 lines
8.8 KiB
PHP
165 lines
8.8 KiB
PHP
<x-page brand>
|
|
{{-- Files this page already uploaded count towards the quota: they can still become a share. --}}
|
|
@if ($isStorageFull && $pendingFiles->isEmpty())
|
|
<x-alert color="warning" :title="__('Storage is full. Uploads are temporarily disabled.')" />
|
|
@else
|
|
<x-form
|
|
wire:submit="createShare"
|
|
x-data="shareUploader({
|
|
csrfToken: {{ \Illuminate\Support\Js::from(csrf_token()) }},
|
|
messages: {{ \Illuminate\Support\Js::from([
|
|
'queued' => __('Waiting'),
|
|
'uploaded' => __('Uploaded'),
|
|
'failed' => __('Upload failed'),
|
|
'sessionExpired' => __('Your session expired. Reload the page to upload again.'),
|
|
]) }},
|
|
})"
|
|
x-on:beforeunload.window="warnBeforeLeaving($event)"
|
|
>
|
|
{{-- WebCrypto, which encrypts the files in the browser, only exists on HTTPS (or localhost). --}}
|
|
<div x-show="! secure" x-cloak data-test="insecure-context">
|
|
<x-alert color="warning" :title="__('Uploads need a secure connection (HTTPS).')" :description="__('Ask the administrator to serve this site over HTTPS.')" />
|
|
</div>
|
|
|
|
{{-- Drop zone: the shape behind the icon turns into a burst while files are over it. --}}
|
|
<div
|
|
class="upload-drop-zone"
|
|
x-bind:data-dragging="dragging ? 'true' : 'false'"
|
|
x-bind:aria-disabled="secure ? 'false' : 'true'"
|
|
x-on:dragover.prevent="dragging = true"
|
|
x-on:dragleave.prevent="dragging = false"
|
|
x-on:drop.prevent="handleDrop($event)"
|
|
data-test="drop-zone"
|
|
>
|
|
<x-stack align="center" gap="space200">
|
|
<div class="upload-drop-shapes">
|
|
<x-shape name="cookie-9" class="upload-drop-shape upload-drop-shape--idle" />
|
|
<x-shape name="soft-burst" class="upload-drop-shape upload-drop-shape--burst" data-test="drop-zone-burst" />
|
|
<x-icon name="upload" size="48" class="upload-drop-icon" />
|
|
</div>
|
|
|
|
<x-stack align="center" gap="space50">
|
|
<p class="md-type-title-md md-text-center">{{ __('Drag & drop files or folders here') }}</p>
|
|
<p class="md-type-body-md md-ink-variant md-text-center">{{ __('or click to browse') }}</p>
|
|
</x-stack>
|
|
|
|
{{-- The button is the tab stop and opens the browser's own picker; the input only carries the selection. --}}
|
|
<x-button :label="__('Browse Files')" icon="folder_open" variant="outlined" x-on:click="$refs.picker.click()" x-bind:disabled="! secure" />
|
|
<input type="file" multiple hidden x-ref="picker" x-on:change="choose($event)" x-bind:disabled="! secure" data-test="file-input" />
|
|
</x-stack>
|
|
</div>
|
|
|
|
{{-- Upload progress, over every file still to send --}}
|
|
<div x-show="busy" x-cloak data-test="upload-progress">
|
|
<x-stack gap="space100">
|
|
<x-row justify="between">
|
|
<span class="md-type-label-lg">{{ __('Uploading...') }} <span x-text="Math.round(progress)"></span>%</span>
|
|
<x-button :label="__('Cancel')" size="xs" x-on:click="cancel()" />
|
|
</x-row>
|
|
<x-progress bind="progress" wavy :label="__('Uploading')" />
|
|
</x-stack>
|
|
</div>
|
|
|
|
@error('files')
|
|
<x-alert color="error">{{ $message }}</x-alert>
|
|
@enderror
|
|
|
|
{{-- Selected files --}}
|
|
@if ($pendingFiles->isNotEmpty())
|
|
<x-stack gap="space100">
|
|
<h2 class="md-type-title-lg">{{ __('Selected Files') }} ({{ $pendingFiles->count() }})</h2>
|
|
|
|
<div class="upload-file-list">
|
|
<x-list segmented :label="__('Selected Files')">
|
|
@foreach ($pendingFiles as $file)
|
|
<x-list-item
|
|
:title="$file->relative_path ?? $file->original_name"
|
|
icon="description"
|
|
wire:key="selected-file-{{ $file->id }}"
|
|
data-test="selected-file"
|
|
>
|
|
<x-slot:description>
|
|
<span class="md-tabular">{{ Number::fileSize($file->file_size) }}</span>
|
|
· <span class="md-tabular" x-text="statusOf({{ $file->id }}, {{ $file->completed_at ? 'true' : 'false' }})" data-test="file-status"></span>
|
|
</x-slot:description>
|
|
<x-slot:end>
|
|
<span x-show="uploads[{{ $file->id }}]?.state === 'failed'" x-cloak>
|
|
<x-button icon="refresh" :aria-label="__('Retry')" x-on:click="retry({{ $file->id }})" />
|
|
</span>
|
|
<x-button icon="close" :aria-label="__('Remove')" x-on:click="remove({{ $file->id }})" />
|
|
</x-slot:end>
|
|
</x-list-item>
|
|
@endforeach
|
|
</x-list>
|
|
</div>
|
|
</x-stack>
|
|
@endif
|
|
|
|
{{-- Options --}}
|
|
<x-card :title="__('Share Options')" heading="h2" variant="outlined">
|
|
<x-stack gap="space200">
|
|
<x-toggle wire:model.live="usePassword" :label="__('Password protect')" right />
|
|
|
|
@if ($usePassword)
|
|
<x-stack gap="space100">
|
|
<x-password full wire:model="password" :label="__('Password')" autocomplete="new-password" />
|
|
|
|
{{-- Generate draws one as Admin settings say (App\Services\PasswordGeneratorService); Copy takes
|
|
whatever is in the field, typed or generated, with the snackbar a copyable field shows. --}}
|
|
<x-row gap="space100" wrap>
|
|
@if ($passwordGeneratorMode !== 'off')
|
|
<x-button :label="__('Generate')" icon="password" variant="tonal" wire:click="generatePassword" spinner="generatePassword" data-test="generate-password" />
|
|
@endif
|
|
|
|
<x-button
|
|
:label="__('Copy')"
|
|
icon="content_copy"
|
|
variant="tonal"
|
|
x-on:click="navigator.clipboard.writeText($wire.password).then(() => window.materialToast({{ \Illuminate\Support\Js::from(__('Copied to the clipboard')) }}, { type: 'success' }))"
|
|
x-bind:disabled="! $wire.password"
|
|
data-test="copy-password"
|
|
/>
|
|
</x-row>
|
|
</x-stack>
|
|
@endif
|
|
|
|
<x-select full
|
|
wire:model="expiration"
|
|
:label="__('Expiration')"
|
|
:placeholder="$allowNeverExpire ? __('Never') : null"
|
|
:options="[
|
|
['id' => '1h', 'name' => __('1 Hour')],
|
|
['id' => '24h', 'name' => __('24 Hours')],
|
|
['id' => '48h', 'name' => __('48 Hours')],
|
|
['id' => '7d', 'name' => __('7 Days')],
|
|
['id' => '14d', 'name' => __('14 Days')],
|
|
['id' => '30d', 'name' => __('30 Days')],
|
|
]"
|
|
/>
|
|
|
|
<x-input full
|
|
wire:model="maxDownloads"
|
|
:label="__('Max downloads')"
|
|
type="number"
|
|
min="1"
|
|
:placeholder="__('Unlimited')"
|
|
/>
|
|
</x-stack>
|
|
</x-card>
|
|
|
|
<x-slot:actions>
|
|
<x-button
|
|
type="submit"
|
|
:label="__('Create Share Link')"
|
|
variant="filled"
|
|
size="md"
|
|
icon="link"
|
|
spinner="createShare"
|
|
x-bind:disabled="busy || {{ $allFilesUploaded ? 'false' : 'true' }}"
|
|
data-test="create-share"
|
|
/>
|
|
</x-slot:actions>
|
|
</x-form>
|
|
@endif
|
|
</x-page>
|