Add SealShare file sharing application with encryption, branding, auth, Docker, and Octane support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4790be8142
commit
e37b322dde
@@ -0,0 +1,70 @@
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold mb-6">{{ __('Admin Dashboard') }}</h1>
|
||||
|
||||
{{-- Stats --}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-sm opacity-60">{{ __('Total Shares') }}</p>
|
||||
<p class="text-2xl font-bold">{{ $totalShares }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-sm opacity-60">{{ __('Active Shares') }}</p>
|
||||
<p class="text-2xl font-bold">{{ $activeShares }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-sm opacity-60">{{ __('Total Files') }}</p>
|
||||
<p class="text-2xl font-bold">{{ $totalFiles }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-sm opacity-60">{{ __('Disk Usage') }}</p>
|
||||
<p class="text-2xl font-bold">{{ Number::fileSize($usedSpace) }}</p>
|
||||
<progress class="progress progress-primary w-full mt-1" value="{{ $maxQuota > 0 ? ($usedSpace / $maxQuota) * 100 : 0 }}" max="100"></progress>
|
||||
<p class="text-xs opacity-50">{{ Number::fileSize($usedSpace) }} / {{ Number::fileSize($maxQuota) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Shares Table --}}
|
||||
<x-card title="{{ __('All Shares') }}" shadow>
|
||||
<x-table :headers="$headers" :rows="$shares" :sort-by="$sortBy" with-pagination>
|
||||
@scope('cell_total_size', $share)
|
||||
{{ Number::fileSize($share->total_size) }}
|
||||
@endscope
|
||||
|
||||
@scope('cell_expires_at', $share)
|
||||
@if ($share->expires_at)
|
||||
<span class="{{ $share->isExpired() ? 'text-error' : '' }}">
|
||||
{{ $share->expires_at->diffForHumans() }}
|
||||
</span>
|
||||
@else
|
||||
<span class="opacity-50">{{ __('Never') }}</span>
|
||||
@endif
|
||||
@endscope
|
||||
|
||||
@scope('cell_created_at', $share)
|
||||
{{ $share->created_at->diffForHumans() }}
|
||||
@endscope
|
||||
|
||||
@scope('actions', $share)
|
||||
<div class="flex gap-1">
|
||||
<a href="{{ route('share.download', $share) }}" class="btn btn-ghost btn-xs" target="_blank">
|
||||
<x-icon name="o-eye" class="w-4 h-4" />
|
||||
</a>
|
||||
<x-button
|
||||
icon="o-trash"
|
||||
class="btn-ghost btn-xs text-error"
|
||||
wire:click="deleteShare({{ $share->id }})"
|
||||
wire:confirm="{{ __('Are you sure you want to delete this share?') }}"
|
||||
/>
|
||||
</div>
|
||||
@endscope
|
||||
</x-table>
|
||||
</x-card>
|
||||
</div>
|
||||
@@ -0,0 +1,151 @@
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-6">{{ __('System Settings') }}</h1>
|
||||
|
||||
@if (session('message'))
|
||||
<div class="alert alert-success mb-6">
|
||||
<x-icon name="o-check-circle" class="w-5 h-5" />
|
||||
<span>{{ session('message') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit="saveSettings">
|
||||
<x-card title="{{ __('Branding') }}" shadow class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<x-input
|
||||
wire:model="siteTitle"
|
||||
label="{{ __('Site Title') }}"
|
||||
hint="{{ __('Displayed as the heading on the upload page.') }}"
|
||||
/>
|
||||
|
||||
<x-textarea
|
||||
wire:model="siteDescription"
|
||||
label="{{ __('Site Description') }}"
|
||||
hint="{{ __('Displayed below the title on the upload page.') }}"
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label class="label label-text font-semibold">{{ __('Logo') }}</label>
|
||||
|
||||
@if ($currentLogo)
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<img src="{{ Storage::disk('public')->url($currentLogo) }}" alt="{{ __('Site Logo') }}" class="h-16 w-auto rounded" />
|
||||
<x-button
|
||||
label="{{ __('Remove Logo') }}"
|
||||
class="btn-sm btn-ghost text-error"
|
||||
wire:click="removeLogo"
|
||||
wire:confirm="{{ __('Remove the logo?') }}"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<input type="file" wire:model="siteLogo" accept="image/*,.svg,.svgz" class="file-input file-input-bordered w-full" />
|
||||
|
||||
@if ($siteLogo && is_object($siteLogo))
|
||||
<div class="mt-2">
|
||||
@if (str_contains($siteLogo->getMimeType(), 'svg'))
|
||||
<p class="text-sm opacity-60">{{ __('SVG selected: :name', ['name' => $siteLogo->getClientOriginalName()]) }}</p>
|
||||
@else
|
||||
<p class="text-sm opacity-60">{{ __('Preview:') }}</p>
|
||||
<img src="{{ $siteLogo->temporaryUrl() }}" alt="{{ __('Logo preview') }}" class="h-16 w-auto rounded mt-1" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@error('siteLogo')
|
||||
<p class="text-error text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
|
||||
<p class="text-xs opacity-50 mt-1">{{ __('Max 2MB. Recommended: PNG or SVG.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
<x-card title="{{ __('Upload Protection') }}" shadow class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<x-password
|
||||
wire:model="systemPassword"
|
||||
label="{{ __('System Upload Password') }}"
|
||||
hint="{{ __('Leave blank to keep current. Set a password to require it before uploading.') }}"
|
||||
/>
|
||||
|
||||
@if ($hasSystemPassword)
|
||||
<div class="mt-2">
|
||||
<x-button
|
||||
label="{{ __('Clear System Password') }}"
|
||||
class="btn-sm btn-ghost text-error"
|
||||
wire:click="clearSystemPassword"
|
||||
wire:confirm="{{ __('Remove the system password?') }}"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
<x-card title="{{ __('Upload Limits') }}" shadow class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<x-toggle
|
||||
wire:model.live="allowNeverExpire"
|
||||
label="{{ __('Allow shares to never expire') }}"
|
||||
hint="{{ __('When disabled, users must select an expiration time.') }}"
|
||||
/>
|
||||
|
||||
<x-select
|
||||
wire:model="defaultExpiration"
|
||||
label="{{ __('Default Expiration') }}"
|
||||
:placeholder="$allowNeverExpire ? __('None') : 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
|
||||
wire:model="maxFileSize"
|
||||
label="{{ __('Max file size (MB)') }}"
|
||||
type="number"
|
||||
min="1"
|
||||
max="{{ $phpMaxUploadMb }}"
|
||||
suffix="MB"
|
||||
hint="{{ __('PHP limit: :max MB (upload_max_filesize / post_max_size)', ['max' => $phpMaxUploadMb]) }}"
|
||||
/>
|
||||
|
||||
<x-input
|
||||
wire:model="maxFilesPerShare"
|
||||
label="{{ __('Max files per share') }}"
|
||||
type="number"
|
||||
min="1"
|
||||
/>
|
||||
|
||||
<x-input
|
||||
wire:model="maxSizePerShare"
|
||||
label="{{ __('Max total size per share (GB)') }}"
|
||||
type="number"
|
||||
min="1"
|
||||
suffix="GB"
|
||||
/>
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
<x-card title="{{ __('Storage') }}" shadow class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<x-input
|
||||
wire:model="maxStorageQuota"
|
||||
label="{{ __('Max storage quota (GB)') }}"
|
||||
type="number"
|
||||
min="1"
|
||||
suffix="GB"
|
||||
hint="{{ __('When reached, new uploads are blocked.') }}"
|
||||
/>
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
<x-button type="submit" label="{{ __('Save Settings') }}" class="btn-primary w-full" icon="o-check" spinner="saveSettings" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,193 @@
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="text-center mb-6">
|
||||
@if ($siteLogo)
|
||||
<img src="{{ Storage::disk('public')->url($siteLogo) }}" alt="{{ $siteTitle ?: config('app.name', 'SealShare') }}" class="h-20 w-auto mx-auto mb-4" />
|
||||
@else
|
||||
<x-app-logo-icon class="h-16 w-16 mx-auto mb-4" />
|
||||
@endif
|
||||
|
||||
<h1 class="text-2xl font-bold">{{ $siteTitle ?: config('app.name', 'SealShare') }}</h1>
|
||||
|
||||
<p class="mt-2 opacity-70">{{ $siteDescription ?: __('Share your files safely and securely') }}</p>
|
||||
</div>
|
||||
|
||||
@if ($isStorageFull)
|
||||
<div class="alert alert-warning mb-6">
|
||||
<x-icon name="o-exclamation-triangle" class="w-5 h-5" />
|
||||
<span>{{ __('Storage is full. Uploads are temporarily disabled.') }}</span>
|
||||
</div>
|
||||
@else
|
||||
<form
|
||||
wire:submit="createShare"
|
||||
x-data="{
|
||||
uploading: false,
|
||||
progress: 0,
|
||||
dragging: false,
|
||||
handleDrop(e) {
|
||||
this.dragging = false;
|
||||
const items = e.dataTransfer.items;
|
||||
const files = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const entry = items[i].webkitGetAsEntry?.();
|
||||
if (entry) {
|
||||
this.traverseEntry(entry, '', files);
|
||||
} else if (items[i].kind === 'file') {
|
||||
files.push({ file: items[i].getAsFile(), path: null });
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const dt = new DataTransfer();
|
||||
const paths = [];
|
||||
files.forEach(f => {
|
||||
dt.items.add(f.file);
|
||||
paths.push(f.path);
|
||||
});
|
||||
$wire.relativePaths = paths;
|
||||
$wire.uploadMultiple('files', dt.files);
|
||||
}, 500);
|
||||
},
|
||||
traverseEntry(entry, path, files) {
|
||||
if (entry.isFile) {
|
||||
entry.file(file => {
|
||||
files.push({ file, path: path ? path + '/' + file.name : null });
|
||||
});
|
||||
} else if (entry.isDirectory) {
|
||||
const reader = entry.createReader();
|
||||
reader.readEntries(entries => {
|
||||
entries.forEach(e => this.traverseEntry(e, path ? path + '/' + entry.name : entry.name, files));
|
||||
});
|
||||
}
|
||||
}
|
||||
}"
|
||||
x-on:livewire-upload-start="uploading = true; progress = 0"
|
||||
x-on:livewire-upload-finish="progress = 100"
|
||||
x-on:livewire-upload-cancel="uploading = false"
|
||||
x-on:livewire-upload-error="uploading = false"
|
||||
x-on:livewire-upload-progress="progress = $event.detail.progress"
|
||||
>
|
||||
{{-- Drop Zone --}}
|
||||
<div
|
||||
class="border-2 border-dashed rounded-xl p-8 text-center transition-colors mb-6"
|
||||
:class="{
|
||||
'border-primary bg-primary/5': dragging,
|
||||
'border-base-300 hover:border-primary/50': !dragging,
|
||||
'opacity-50 pointer-events-none': uploading
|
||||
}"
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave.prevent="dragging = false"
|
||||
@drop.prevent="handleDrop($event)"
|
||||
>
|
||||
<x-icon name="o-cloud-arrow-up" class="w-12 h-12 mx-auto opacity-40 mb-3" />
|
||||
<p class="font-medium">{{ __('Drag & drop files or folders here') }}</p>
|
||||
<p class="text-sm opacity-60 mt-1">{{ __('or click to browse') }}</p>
|
||||
|
||||
<label class="btn btn-outline btn-sm mt-4 cursor-pointer" :class="uploading && 'btn-disabled'">
|
||||
{{ __('Browse Files') }}
|
||||
<input
|
||||
type="file"
|
||||
wire:model="files"
|
||||
multiple
|
||||
class="hidden"
|
||||
:disabled="uploading"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- Upload Progress --}}
|
||||
<div x-show="uploading" x-cloak class="mb-6">
|
||||
<template x-if="progress < 100">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium">{{ __('Uploading...') }} <span x-text="Math.round(progress)"></span>%</span>
|
||||
<button type="button" class="btn btn-ghost btn-xs" x-on:click="$wire.cancelUpload('files')">
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
<progress class="progress progress-primary w-full" max="100" x-bind:value="progress"></progress>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="progress >= 100">
|
||||
<div class="flex items-center gap-3 text-sm font-medium">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{{ __('Processing files...') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Errors --}}
|
||||
@error('files')
|
||||
<div x-init="uploading = false" class="alert alert-error mb-4">{{ $message }}</div>
|
||||
@enderror
|
||||
|
||||
{{-- File List --}}
|
||||
@if (count($files))
|
||||
<div x-init="uploading = false" class="mb-6">
|
||||
<h3 class="font-semibold mb-2">{{ __('Selected Files') }} ({{ count($files) }})</h3>
|
||||
<div class="space-y-1 max-h-60 overflow-y-auto">
|
||||
@foreach ($files as $index => $file)
|
||||
<div class="flex items-center justify-between px-3 py-2 rounded-lg bg-base-200 text-sm">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<x-icon name="o-document" class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="truncate">
|
||||
{{ $relativePaths[$index] ?? $file->getClientOriginalName() }}
|
||||
</span>
|
||||
<span class="opacity-50 flex-shrink-0">
|
||||
({{ Number::fileSize($file->getSize()) }})
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" wire:click="removeFile({{ $index }})" class="btn btn-ghost btn-xs">
|
||||
<x-icon name="o-x-mark" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Options --}}
|
||||
<x-card title="{{ __('Share Options') }}" class="mb-6" shadow>
|
||||
<div class="space-y-4">
|
||||
<x-toggle wire:model.live="usePassword" label="{{ __('Password protect') }}" />
|
||||
|
||||
@if ($usePassword)
|
||||
<x-password wire:model="password" label="{{ __('Password') }}" />
|
||||
@endif
|
||||
|
||||
<x-select
|
||||
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
|
||||
wire:model="maxDownloads"
|
||||
label="{{ __('Max downloads') }}"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="{{ __('Unlimited') }}"
|
||||
/>
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
{{-- Submit --}}
|
||||
<x-button
|
||||
type="submit"
|
||||
label="{{ __('Create Share Link') }}"
|
||||
class="btn-primary w-full"
|
||||
icon="o-link"
|
||||
spinner="createShare"
|
||||
x-bind:disabled="uploading || {{ count($files) === 0 ? 'true' : 'false' }}"
|
||||
/>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Setup SealShare')" :description="__('Create your admin account to get started')" />
|
||||
|
||||
<form wire:submit="createAdmin" class="flex flex-col gap-6">
|
||||
<x-input
|
||||
wire:model="name"
|
||||
label="{{ __('Name') }}"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
placeholder="{{ __('Admin name') }}"
|
||||
icon="o-user"
|
||||
/>
|
||||
|
||||
<x-input
|
||||
wire:model="email"
|
||||
label="{{ __('Email address') }}"
|
||||
type="email"
|
||||
required
|
||||
placeholder="admin@example.com"
|
||||
icon="o-envelope"
|
||||
/>
|
||||
|
||||
<x-password
|
||||
wire:model="password"
|
||||
label="{{ __('Password') }}"
|
||||
required
|
||||
placeholder="{{ __('Password') }}"
|
||||
/>
|
||||
|
||||
<x-password
|
||||
wire:model="password_confirmation"
|
||||
label="{{ __('Confirm password') }}"
|
||||
required
|
||||
placeholder="{{ __('Confirm password') }}"
|
||||
/>
|
||||
|
||||
<x-button type="submit" label="{{ __('Create Admin Account') }}" class="btn-primary w-full" spinner="createAdmin" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
<div class="max-w-lg mx-auto">
|
||||
<x-card title="{{ __('Share Created!') }}" subtitle="{{ __('Your files are ready to share') }}" shadow>
|
||||
<div class="space-y-4">
|
||||
{{-- Share URL --}}
|
||||
<div
|
||||
x-data="{
|
||||
copied: false,
|
||||
url: '{{ route('share.download', $share) }}',
|
||||
async copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.url);
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 2000);
|
||||
} catch (e) {}
|
||||
}
|
||||
}"
|
||||
>
|
||||
<label class="label font-medium text-sm">{{ __('Share Link') }}</label>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
:value="url"
|
||||
class="input input-bordered join-item w-full"
|
||||
/>
|
||||
<button type="button" @click="copy()" class="btn join-item">
|
||||
<span x-show="!copied">{{ __('Copy') }}</span>
|
||||
<span x-show="copied" class="text-success">{{ __('Copied!') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Details --}}
|
||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||
<div class="bg-base-200 rounded-lg p-3">
|
||||
<span class="opacity-60">{{ __('Files') }}</span>
|
||||
<p class="font-semibold">{{ $share->files->count() }}</p>
|
||||
</div>
|
||||
<div class="bg-base-200 rounded-lg p-3">
|
||||
<span class="opacity-60">{{ __('Total Size') }}</span>
|
||||
<p class="font-semibold">{{ Number::fileSize($share->total_size) }}</p>
|
||||
</div>
|
||||
<div class="bg-base-200 rounded-lg p-3">
|
||||
<span class="opacity-60">{{ __('Expires') }}</span>
|
||||
<p class="font-semibold">{{ $share->expires_at ? $share->expires_at->diffForHumans() : __('Never') }}</p>
|
||||
</div>
|
||||
<div class="bg-base-200 rounded-lg p-3">
|
||||
<span class="opacity-60">{{ __('Max Downloads') }}</span>
|
||||
<p class="font-semibold">{{ $share->max_downloads ?? __('Unlimited') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($share->isPasswordProtected())
|
||||
<div class="alert alert-info">
|
||||
<x-icon name="o-lock-closed" class="w-5 h-5" />
|
||||
<span>{{ __('This share is password protected') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button label="{{ __('Upload More') }}" link="{{ route('upload') }}" icon="o-plus" />
|
||||
</x-slot:actions>
|
||||
</x-card>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
<div class="w-full max-w-lg mx-auto">
|
||||
<div class="text-center mb-6">
|
||||
@if ($siteLogo)
|
||||
<img src="{{ Storage::disk('public')->url($siteLogo) }}" alt="{{ $siteTitle ?: config('app.name', 'SealShare') }}" class="h-20 w-auto mx-auto mb-4" />
|
||||
@else
|
||||
<x-app-logo-icon class="h-16 w-16 mx-auto mb-4" />
|
||||
@endif
|
||||
|
||||
<h1 class="text-2xl font-bold">{{ $siteTitle ?: config('app.name', 'SealShare') }}</h1>
|
||||
|
||||
<p class="mt-2 opacity-70">{{ $siteDescription ?: __('Share your files safely and securely') }}</p>
|
||||
</div>
|
||||
|
||||
@if (! $authenticated)
|
||||
{{-- Password form --}}
|
||||
<x-card title="{{ __('Password Required') }}" subtitle="{{ __('Enter the password to access these files') }}" shadow>
|
||||
<form wire:submit="verifyPassword" class="space-y-4">
|
||||
<x-password
|
||||
wire:model="password"
|
||||
label="{{ __('Password') }}"
|
||||
required
|
||||
placeholder="{{ __('Enter share password') }}"
|
||||
/>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button type="submit" label="{{ __('Unlock') }}" class="btn-primary" icon="o-lock-open" spinner="verifyPassword" />
|
||||
</x-slot:actions>
|
||||
</form>
|
||||
</x-card>
|
||||
@else
|
||||
{{-- File list --}}
|
||||
<x-card title="{{ __('Shared Files') }}" shadow>
|
||||
<div class="space-y-2 mb-4">
|
||||
@foreach ($share->files as $file)
|
||||
<div class="flex items-center justify-between px-3 py-2 rounded-lg bg-base-200 text-sm">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<x-icon name="o-document" class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ $file->relative_path ?: $file->original_name }}</span>
|
||||
<span class="opacity-50 flex-shrink-0">({{ Number::fileSize($file->file_size) }})</span>
|
||||
</div>
|
||||
<a href="{{ route('share.download.file', [$share, $file]) }}" class="btn btn-ghost btn-xs">
|
||||
<x-icon name="o-arrow-down-tray" class="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($share->expires_at)
|
||||
<p class="text-xs opacity-50 mb-2">
|
||||
{{ __('Expires') }}: {{ $share->expires_at->diffForHumans() }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<x-slot:actions>
|
||||
@if ($share->files->count() > 1)
|
||||
<a href="{{ route('share.download.all', $share) }}" class="btn btn-primary">
|
||||
<x-icon name="o-arrow-down-tray" class="w-4 h-4" />
|
||||
{{ __('Download All as ZIP') }}
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('share.download.file', [$share, $share->files->first()]) }}" class="btn btn-primary">
|
||||
<x-icon name="o-arrow-down-tray" class="w-4 h-4" />
|
||||
{{ __('Download') }}
|
||||
</a>
|
||||
@endif
|
||||
</x-slot:actions>
|
||||
</x-card>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('System Password Required')" :description="__('Enter the system password to access the upload page')" />
|
||||
|
||||
<form wire:submit="verify" class="flex flex-col gap-6">
|
||||
<x-password
|
||||
wire:model="password"
|
||||
label="{{ __('Password') }}"
|
||||
required
|
||||
placeholder="{{ __('System password') }}"
|
||||
/>
|
||||
|
||||
<x-button type="submit" label="{{ __('Continue') }}" class="btn-primary w-full" spinner="verify" />
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user