Move SealShare onto Livewire Material
Replaces maryUI and daisyUI with nonameweb/livewire-material: the Vibrant indigo scheme, a system/light/dark theme under sealshare-theme, one top app bar with the account menu, the upload drop zone and link-ready moments, M3 fields, dialogs instead of wire:confirm, snackbars instead of flashed messages, a sortable admin table, and the starter-kit cleanup. Docker builds assets after Composer; CI drops the Flux step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
co-authored by
Claude Opus 5
parent
6d7120e8e2
commit
c4a17b65c8
@@ -1,14 +0,0 @@
|
||||
@props([
|
||||
'on',
|
||||
])
|
||||
|
||||
<div
|
||||
x-data="{ shown: false, timeout: null }"
|
||||
x-init="@this.on('{{ $on }}', () => { clearTimeout(timeout); shown = true; timeout = setTimeout(() => { shown = false }, 2000); })"
|
||||
x-show.transition.out.opacity.duration.1500ms="shown"
|
||||
x-transition:leave.opacity.duration.1500ms
|
||||
style="display: none"
|
||||
{{ $attributes->merge(['class' => 'text-sm']) }}
|
||||
>
|
||||
{{ $slot->isEmpty() ? __('Saved.') : $slot }}
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
<a href="{{ route('home') }}" {{ $attributes->merge(['class' => 'flex items-center gap-2 font-semibold']) }} wire:navigate>
|
||||
<x-app-logo-icon class="size-6 fill-current" />
|
||||
<span>{{ \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare') }}</span>
|
||||
</a>
|
||||
@@ -4,6 +4,6 @@
|
||||
])
|
||||
|
||||
<div class="flex w-full flex-col text-center">
|
||||
<h2 class="text-xl font-bold">{{ $title }}</h2>
|
||||
<p class="text-sm opacity-60 mt-1">{{ $description }}</p>
|
||||
<h1 class="type-headline-sm">{{ $title }}</h1>
|
||||
<p class="mt-1 type-body-md text-on-surface-variant">{{ $description }}</p>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,5 @@
|
||||
])
|
||||
|
||||
@if ($status)
|
||||
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
|
||||
{{ $status }}
|
||||
</div>
|
||||
<x-alert color="success" {{ $attributes }}>{{ $status }}</x-alert>
|
||||
@endif
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{{-- Desktop user menu - integrated into sidebar layout --}}
|
||||
@@ -1,12 +0,0 @@
|
||||
@props([
|
||||
'id' => uniqid(),
|
||||
])
|
||||
|
||||
<svg {{ $attributes }} fill="none">
|
||||
<defs>
|
||||
<pattern id="pattern-{{ $id }}" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
|
||||
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect stroke="none" fill="url(#pattern-{{ $id }})" width="100%" height="100%"></rect>
|
||||
</svg>
|
||||
@@ -1,18 +0,0 @@
|
||||
<x-layouts::app :title="__('Dashboard')">
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-base-300">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-current/20" />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-base-300">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-current/20" />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-base-300">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-current/20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-base-300">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-current/20" />
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts::app>
|
||||
@@ -1,3 +1,15 @@
|
||||
<x-layouts::app.sidebar :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts::app.sidebar>
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-dvh bg-surface font-sans text-on-surface antialiased">
|
||||
@include('partials.app-bar')
|
||||
|
||||
<main class="mx-auto w-full max-w-5xl px-4 pt-4 pb-16 sm:px-6">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
|
||||
<x-toast />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{{-- Header layout not used - redirects to sidebar layout --}}
|
||||
<x-layouts::app.sidebar :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts::app.sidebar>
|
||||
@@ -1,42 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="dark">
|
||||
<script>document.documentElement.setAttribute('data-theme', localStorage.getItem('mary-theme')?.replaceAll('"','') || 'dark')</script>
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen font-sans antialiased bg-base-200/50 flex flex-col">
|
||||
|
||||
{{-- MAIN CONTENT --}}
|
||||
<main class="flex-1 w-full max-w-5xl mx-auto px-4 py-8">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
|
||||
{{-- FOOTER NAV --}}
|
||||
<footer class="border-t border-base-300 bg-base-100/50">
|
||||
<div class="max-w-5xl mx-auto px-4 py-3 flex items-center justify-between text-sm">
|
||||
<a href="{{ route('upload') }}" class="font-medium opacity-70 hover:opacity-100 transition-opacity">
|
||||
{{ \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare') }}
|
||||
</a>
|
||||
<nav class="flex items-center gap-4">
|
||||
<x-theme-toggle class="opacity-60 hover:opacity-100 transition-opacity" />
|
||||
@auth
|
||||
@if(auth()->user()->is_admin)
|
||||
<a href="{{ route('admin.dashboard') }}" class="opacity-60 hover:opacity-100 transition-opacity">{{ __('Dashboard') }}</a>
|
||||
<a href="{{ route('admin.settings') }}" class="opacity-60 hover:opacity-100 transition-opacity">{{ __('Settings') }}</a>
|
||||
@endif
|
||||
<a href="{{ route('profile.edit') }}" class="opacity-60 hover:opacity-100 transition-opacity">{{ __('Profile') }}</a>
|
||||
<form method="POST" action="{{ route('logout') }}" class="inline">
|
||||
@csrf
|
||||
<button type="submit" class="opacity-60 hover:opacity-100 transition-opacity">{{ __('Logout') }}</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="opacity-60 hover:opacity-100 transition-opacity">{{ __('Login') }}</a>
|
||||
@endauth
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{{-- Toast --}}
|
||||
<x-toast />
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,19 @@
|
||||
<x-layouts::auth.simple :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts::auth.simple>
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="flex min-h-dvh flex-col bg-surface font-sans text-on-surface antialiased">
|
||||
@include('partials.app-bar')
|
||||
|
||||
<main class="flex flex-1 justify-center px-4 pt-6 pb-16 sm:items-center sm:pt-0">
|
||||
<div class="w-full max-w-md rounded-corner-xl bg-surface-container-low p-6 sm:p-8">
|
||||
<div class="flex flex-col gap-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<x-toast />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{{-- Card auth layout - delegates to simple layout --}}
|
||||
<x-layouts::auth.simple :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts::auth.simple>
|
||||
@@ -1,27 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="dark">
|
||||
<script>document.documentElement.setAttribute('data-theme', localStorage.getItem('mary-theme')?.replaceAll('"','') || 'dark')</script>
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-200 antialiased">
|
||||
<div class="flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div class="flex w-full max-w-sm flex-col gap-2">
|
||||
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
|
||||
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current" />
|
||||
</span>
|
||||
<span class="sr-only">{{ \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare') }}</span>
|
||||
</a>
|
||||
<div class="flex flex-col gap-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<x-theme-toggle class="opacity-60 hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +0,0 @@
|
||||
{{-- Split auth layout - delegates to simple layout --}}
|
||||
<x-layouts::auth.simple :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts::auth.simple>
|
||||
@@ -1,70 +1,69 @@
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold mb-6">{{ __('Admin Dashboard') }}</h1>
|
||||
<h1 class="mb-6 type-headline-md">{{ __('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 class="mb-6 grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<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" class="mt-2" :label="__('Disk Usage')" />
|
||||
</x-stat>
|
||||
</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
|
||||
<x-card :title="__('All Shares')" variant="outlined">
|
||||
<div class="-mx-4 overflow-x-auto">
|
||||
<x-table>
|
||||
<thead>
|
||||
<tr>
|
||||
<x-sort-header column="token" :sort-by="$sortBy">{{ __('Token') }}</x-sort-header>
|
||||
<x-sort-header column="files_count" :sort-by="$sortBy" class="text-end">{{ __('Files') }}</x-sort-header>
|
||||
<x-sort-header column="total_size" :sort-by="$sortBy" class="text-end">{{ __('Size') }}</x-sort-header>
|
||||
<x-sort-header column="download_count" :sort-by="$sortBy" class="text-end">{{ __('Downloads') }}</x-sort-header>
|
||||
<x-sort-header column="expires_at" :sort-by="$sortBy">{{ __('Expires') }}</x-sort-header>
|
||||
<x-sort-header column="created_at" :sort-by="$sortBy">{{ __('Created') }}</x-sort-header>
|
||||
<th><span class="sr-only">{{ __('Actions') }}</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($shares as $share)
|
||||
<tr wire:key="share-{{ $share->id }}">
|
||||
<td class="font-mono">{{ $share->token }}</td>
|
||||
<td class="text-end tabular-nums">{{ $share->files_count }}</td>
|
||||
<td class="text-end tabular-nums whitespace-nowrap">{{ Number::fileSize($share->total_size) }}</td>
|
||||
<td class="text-end tabular-nums">{{ $share->download_count }}</td>
|
||||
<td class="whitespace-nowrap">
|
||||
@if ($share->expires_at)
|
||||
<span @class(['text-error' => $share->isExpired()])>{{ $share->expires_at->diffForHumans() }}</span>
|
||||
@else
|
||||
<span class="text-on-surface-variant">{{ __('Never') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="whitespace-nowrap">{{ $share->created_at->diffForHumans() }}</td>
|
||||
<td class="text-end whitespace-nowrap">
|
||||
<x-button icon="open_in_new" :tooltip="__('Open')" :link="route('share.download', $share)" external />
|
||||
<x-button icon="delete" :tooltip="__('Delete')" color="error" wire:click="$set('deletingShareId', {{ $share->id }})" data-test="delete-share-{{ $share->id }}" />
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<x-empty-state icon="link_off" :title="__('No shares yet')" :description="__('Shares appear here once someone uploads files.')" />
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</x-table>
|
||||
</div>
|
||||
|
||||
@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>
|
||||
<div class="mt-4">{{ $shares->links() }}</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -1,100 +1,66 @@
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-6">{{ __('System Settings') }}</h1>
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="mb-6 type-headline-md">{{ __('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" class="grid gap-6">
|
||||
<x-card :title="__('Branding')" variant="outlined">
|
||||
<div class="grid gap-5">
|
||||
<x-input wire:model="siteTitle" :label="__('Site Title')" :hint="__('Displayed as the heading on the upload page.')" />
|
||||
|
||||
<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>
|
||||
<x-textarea wire:model="siteDescription" :label="__('Site Description')" :hint="__('Displayed below the title on the upload page.')" rows="3" />
|
||||
|
||||
<div class="grid gap-3">
|
||||
@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 class="flex flex-wrap items-center gap-4">
|
||||
<img src="{{ Storage::disk('public')->url($currentLogo) }}" alt="{{ __('Site Logo') }}" class="h-16 w-auto rounded-corner-sm" />
|
||||
<x-button :label="__('Remove Logo')" icon="delete" color="error" wire:click="$set('confirmingLogoRemoval', true)" data-test="remove-logo" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<input type="file" wire:model="siteLogo" accept="image/*,.svg,.svgz" class="file-input file-input-bordered w-full" />
|
||||
<x-file wire:model="siteLogo" :label="__('Logo')" accept="image/*,.svg,.svgz" :hint="__('Max 2MB. Recommended: PNG or SVG.')" />
|
||||
|
||||
@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>
|
||||
@if (str_contains($siteLogo->getMimeType(), 'svg'))
|
||||
<p class="type-body-md text-on-surface-variant">{{ __('SVG selected: :name', ['name' => $siteLogo->getClientOriginalName()]) }}</p>
|
||||
@else
|
||||
<div>
|
||||
<p class="type-label-lg text-on-surface-variant">{{ __('Preview:') }}</p>
|
||||
<img src="{{ $siteLogo->temporaryUrl() }}" alt="{{ __('Logo preview') }}" class="mt-1 h-16 w-auto rounded-corner-sm" />
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
<x-card title="{{ __('Upload Limits') }}" shadow class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<x-card :title="__('Upload Protection')" variant="outlined">
|
||||
<div class="grid gap-3">
|
||||
<x-password
|
||||
wire:model="systemPassword"
|
||||
:label="__('System Upload Password')"
|
||||
:hint="__('Leave blank to keep current. Set a password to require it before uploading.')"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
@if ($hasSystemPassword)
|
||||
<div>
|
||||
<x-button :label="__('Clear System Password')" icon="lock_reset" color="error" wire:click="$set('confirmingPasswordRemoval', true)" data-test="clear-system-password" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
<x-card :title="__('Upload Limits')" variant="outlined">
|
||||
<div class="grid gap-5">
|
||||
<x-toggle
|
||||
wire:model.live="allowNeverExpire"
|
||||
label="{{ __('Allow shares to never expire') }}"
|
||||
hint="{{ __('When disabled, users must select an expiration time.') }}"
|
||||
:label="__('Allow shares to never expire')"
|
||||
:hint="__('When disabled, users must select an expiration time.')"
|
||||
right
|
||||
/>
|
||||
|
||||
<x-select
|
||||
wire:model="defaultExpiration"
|
||||
label="{{ __('Default Expiration') }}"
|
||||
:label="__('Default Expiration')"
|
||||
:placeholder="$allowNeverExpire ? __('None') : null"
|
||||
:options="[
|
||||
['id' => '1h', 'name' => __('1 Hour')],
|
||||
@@ -108,44 +74,49 @@
|
||||
|
||||
<x-input
|
||||
wire:model="maxFileSize"
|
||||
label="{{ __('Max file size (MB)') }}"
|
||||
:label="__('Max file size (MB)')"
|
||||
type="number"
|
||||
min="1"
|
||||
max="{{ $phpMaxUploadMb }}"
|
||||
:max="$phpMaxUploadMb"
|
||||
suffix="MB"
|
||||
hint="{{ __('PHP limit: :max MB (upload_max_filesize / post_max_size)', ['max' => $phpMaxUploadMb]) }}"
|
||||
: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="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"
|
||||
/>
|
||||
<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 :title="__('Storage')" variant="outlined">
|
||||
<x-input
|
||||
wire:model="maxStorageQuota"
|
||||
:label="__('Max storage quota (GB)')"
|
||||
type="number"
|
||||
min="1"
|
||||
suffix="GB"
|
||||
:hint="__('When reached, new uploads are blocked.')"
|
||||
/>
|
||||
</x-card>
|
||||
|
||||
<x-button type="submit" label="{{ __('Save Settings') }}" class="btn-primary w-full" icon="o-check" spinner="saveSettings" />
|
||||
<x-button type="submit" :label="__('Save Settings')" variant="filled" icon="check" spinner="saveSettings" class="w-full" />
|
||||
</form>
|
||||
|
||||
<x-modal wire:model="confirmingLogoRemoval" :title="__('Remove the logo?')" icon="delete">
|
||||
{{ __('The upload and download pages show the default mark again.') }}
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button :label="__('Cancel')" x-on:click="close()" />
|
||||
<x-button :label="__('Remove')" danger wire:click="removeLogo" data-test="confirm-remove-logo" />
|
||||
</x-slot:actions>
|
||||
</x-modal>
|
||||
|
||||
<x-modal wire:model="confirmingPasswordRemoval" :title="__('Remove the system password?')" icon="lock_open">
|
||||
{{ __('Anyone who can reach the upload page can upload files again.') }}
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button :label="__('Cancel')" x-on:click="close()" />
|
||||
<x-button :label="__('Remove')" danger wire:click="clearSystemPassword" data-test="confirm-clear-system-password" />
|
||||
</x-slot:actions>
|
||||
</x-modal>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="text-center mb-6">
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<div class="mb-8 text-center">
|
||||
@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" />
|
||||
<img src="{{ Storage::disk('public')->url($siteLogo) }}" alt="{{ $siteTitle ?: config('app.name', 'SealShare') }}" class="mx-auto mb-4 h-20 w-auto" />
|
||||
@endif
|
||||
|
||||
<h1 class="text-2xl font-bold">{{ $siteTitle ?: config('app.name', 'SealShare') }}</h1>
|
||||
<h1 class="type-headline-lg">{{ $siteTitle ?: config('app.name', 'SealShare') }}</h1>
|
||||
|
||||
<p class="mt-2 opacity-70">{{ $siteDescription ?: __('Share your files safely and securely') }}</p>
|
||||
<p class="mt-2 type-body-lg text-on-surface-variant">{{ $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>
|
||||
<x-alert color="warning" :title="__('Storage is full. Uploads are temporarily disabled.')" />
|
||||
@else
|
||||
<form
|
||||
wire:submit="createShare"
|
||||
@@ -88,97 +83,98 @@
|
||||
x-on:livewire-upload-error="resetUpload()"
|
||||
x-on:livewire-upload-progress="progress = $event.detail.progress"
|
||||
>
|
||||
{{-- Drop Zone --}}
|
||||
{{-- Drop zone: the shape behind the icon turns into a burst while files are over it. --}}
|
||||
<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
|
||||
class="mb-6 rounded-corner-xl border-2 border-dashed p-8 text-center transition-colors duration-(--md-sys-motion-effects-default-duration) ease-effects-default"
|
||||
x-bind:class="{
|
||||
'border-primary bg-primary-container/40': dragging,
|
||||
'border-outline-variant': ! dragging,
|
||||
'pointer-events-none opacity-60': uploading,
|
||||
}"
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave.prevent="dragging = false"
|
||||
@drop.prevent="handleDrop($event)"
|
||||
x-on:dragover.prevent="dragging = true"
|
||||
x-on:dragleave.prevent="dragging = false"
|
||||
x-on:drop.prevent="handleDrop($event)"
|
||||
data-test="drop-zone"
|
||||
>
|
||||
<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>
|
||||
<div class="relative mx-auto mb-4 grid size-28 place-items-center">
|
||||
<span
|
||||
class="absolute inset-0 transition-[scale,rotate,opacity] duration-(--md-sys-motion-spatial-slow-duration) ease-spatial-slow motion-reduce:transition-none"
|
||||
x-bind:class="dragging ? 'scale-50 rotate-45 opacity-0' : 'scale-100 rotate-0 opacity-100'"
|
||||
><x-shape name="cookie-9" class="size-full text-secondary-container" /></span>
|
||||
<span
|
||||
class="absolute inset-0 transition-[scale,rotate,opacity] duration-(--md-sys-motion-spatial-slow-duration) ease-spatial-slow motion-reduce:transition-none"
|
||||
x-bind:class="dragging ? 'scale-110 rotate-0 opacity-100' : 'scale-50 -rotate-45 opacity-0'"
|
||||
><x-shape name="soft-burst" class="size-full text-primary-container" /></span>
|
||||
<x-icon name="upload" class="relative size-12 text-on-secondary-container" x-bind:class="dragging && 'text-on-primary-container'" />
|
||||
</div>
|
||||
|
||||
<label class="btn btn-outline btn-sm mt-4 cursor-pointer" :class="uploading && 'btn-disabled'">
|
||||
<p class="type-title-md">{{ __('Drag & drop files or folders here') }}</p>
|
||||
<p class="mt-1 type-body-md text-on-surface-variant">{{ __('or click to browse') }}</p>
|
||||
|
||||
<label
|
||||
class="state-layer focus-ring mt-4 inline-flex h-10 cursor-pointer items-center gap-2 rounded-corner-full border border-outline-variant px-4 type-label-lg text-primary has-focus-visible:outline-3 has-focus-visible:outline-secondary"
|
||||
x-bind:class="uploading && 'pointer-events-none opacity-38'"
|
||||
>
|
||||
<x-icon name="folder_open" class="size-5" />
|
||||
{{ __('Browse Files') }}
|
||||
<input
|
||||
type="file"
|
||||
wire:model="files"
|
||||
multiple
|
||||
class="hidden"
|
||||
:disabled="uploading"
|
||||
/>
|
||||
<input type="file" wire:model="files" multiple class="sr-only" x-bind: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>
|
||||
{{-- Upload progress --}}
|
||||
<div x-show="uploading" x-cloak class="mb-6" data-test="upload-progress">
|
||||
<div x-show="progress < 100">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="type-label-lg">{{ __('Uploading...') }} <span x-text="Math.round(progress)"></span>%</span>
|
||||
<x-button :label="__('Cancel')" size="xs" x-on:click="$wire.cancelUpload('files')" />
|
||||
</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>
|
||||
<x-progress bind="progress" wavy :label="__('Uploading')" />
|
||||
</div>
|
||||
<div x-show="progress >= 100" class="flex items-center gap-3 type-label-lg">
|
||||
<x-loading class="size-8" :label="false" />
|
||||
{{ __('Processing files...') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Errors --}}
|
||||
@error('files')
|
||||
<div class="alert alert-error mb-4">{{ $message }}</div>
|
||||
<x-alert color="error" class="mb-4">{{ $message }}</x-alert>
|
||||
@enderror
|
||||
|
||||
{{-- File List --}}
|
||||
{{-- Selected files --}}
|
||||
@if (count($files))
|
||||
<div 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
|
||||
<h2 class="mb-2 type-title-md">{{ __('Selected Files') }} ({{ count($files) }})</h2>
|
||||
<div class="max-h-72 overflow-y-auto">
|
||||
<x-list segmented :label="__('Selected Files')">
|
||||
@foreach ($files as $index => $file)
|
||||
<x-list-item
|
||||
:title="$relativePaths[$index] ?? $file->getClientOriginalName()"
|
||||
:description="Number::fileSize($file->getSize())"
|
||||
icon="description"
|
||||
wire:key="selected-file-{{ $index }}"
|
||||
>
|
||||
<x-slot:end>
|
||||
<x-button icon="close" :aria-label="__('Remove')" wire:click="removeFile({{ $index }})" />
|
||||
</x-slot:end>
|
||||
</x-list-item>
|
||||
@endforeach
|
||||
</x-list>
|
||||
</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') }}" />
|
||||
<x-card :title="__('Share Options')" variant="outlined" class="mb-6">
|
||||
<div class="grid gap-5">
|
||||
<x-toggle wire:model.live="usePassword" :label="__('Password protect')" right />
|
||||
|
||||
@if ($usePassword)
|
||||
<x-password wire:model="password" label="{{ __('Password') }}" />
|
||||
<x-password wire:model="password" :label="__('Password')" autocomplete="new-password" />
|
||||
@endif
|
||||
|
||||
<x-select
|
||||
wire:model="expiration"
|
||||
label="{{ __('Expiration') }}"
|
||||
:label="__('Expiration')"
|
||||
:placeholder="$allowNeverExpire ? __('Never') : null"
|
||||
:options="[
|
||||
['id' => '1h', 'name' => __('1 Hour')],
|
||||
@@ -192,22 +188,24 @@
|
||||
|
||||
<x-input
|
||||
wire:model="maxDownloads"
|
||||
label="{{ __('Max downloads') }}"
|
||||
:label="__('Max downloads')"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="{{ __('Unlimited') }}"
|
||||
:placeholder="__('Unlimited')"
|
||||
/>
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
{{-- Submit --}}
|
||||
<x-button
|
||||
type="submit"
|
||||
label="{{ __('Create Share Link') }}"
|
||||
class="btn-primary w-full"
|
||||
icon="o-link"
|
||||
:label="__('Create Share Link')"
|
||||
variant="filled"
|
||||
size="md"
|
||||
class="w-full"
|
||||
icon="link"
|
||||
spinner="createShare"
|
||||
x-bind:disabled="uploading || {{ count($files) === 0 ? 'true' : 'false' }}"
|
||||
data-test="create-share"
|
||||
/>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@@ -4,37 +4,37 @@
|
||||
<form wire:submit="createAdmin" class="flex flex-col gap-6">
|
||||
<x-input
|
||||
wire:model="name"
|
||||
label="{{ __('Name') }}"
|
||||
:label="__('Name')"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
placeholder="{{ __('Admin name') }}"
|
||||
icon="o-user"
|
||||
:placeholder="__('Admin name')"
|
||||
icon="person"
|
||||
/>
|
||||
|
||||
<x-input
|
||||
wire:model="email"
|
||||
label="{{ __('Email address') }}"
|
||||
:label="__('Email address')"
|
||||
type="email"
|
||||
required
|
||||
placeholder="admin@example.com"
|
||||
icon="o-envelope"
|
||||
icon="mail"
|
||||
/>
|
||||
|
||||
<x-password
|
||||
wire:model="password"
|
||||
label="{{ __('Password') }}"
|
||||
:label="__('Password')"
|
||||
required
|
||||
placeholder="{{ __('Password') }}"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
<x-password
|
||||
wire:model="password_confirmation"
|
||||
label="{{ __('Confirm password') }}"
|
||||
:label="__('Confirm password')"
|
||||
required
|
||||
placeholder="{{ __('Confirm password') }}"
|
||||
:placeholder="__('Confirm password')"
|
||||
/>
|
||||
|
||||
<x-button type="submit" label="{{ __('Create Admin Account') }}" class="btn-primary w-full" spinner="createAdmin" />
|
||||
<x-button type="submit" :label="__('Create Admin Account')" variant="filled" class="w-full" spinner="createAdmin" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,65 +1,38 @@
|
||||
<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 class="mx-auto max-w-lg">
|
||||
<div class="mb-8 text-center">
|
||||
{{-- The link is ready: a check on an Expressive shape that settles in. --}}
|
||||
<div class="relative mx-auto mb-4 grid size-24 place-items-center motion-safe:animate-[share-ready_var(--md-sys-motion-spatial-slow-duration)_var(--md-sys-motion-spatial-slow)_both]">
|
||||
<x-shape name="soft-burst" class="absolute inset-0 size-full text-primary-container" />
|
||||
<x-icon name="check" class="relative size-12 text-on-primary-container" />
|
||||
</div>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button label="{{ __('Upload More') }}" link="{{ route('upload') }}" icon="o-plus" />
|
||||
</x-slot:actions>
|
||||
</x-card>
|
||||
<h1 class="type-headline-md">{{ __('Share Created!') }}</h1>
|
||||
<p class="mt-1 type-body-lg text-on-surface-variant">{{ __('Your files are ready to share') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4">
|
||||
<x-input
|
||||
:label="__('Share Link')"
|
||||
:value="route('share.download', $share)"
|
||||
readonly
|
||||
copyable
|
||||
icon="link"
|
||||
data-test="share-link"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<x-stat :title="__('Files')" :value="$share->files->count()" icon="description" />
|
||||
<x-stat :title="__('Total Size')" :value="Number::fileSize($share->total_size)" icon="hard_drive" />
|
||||
<x-stat :title="__('Expires')" :value="$share->expires_at ? $share->expires_at->diffForHumans() : __('Never')" icon="schedule" />
|
||||
<x-stat :title="__('Max Downloads')" :value="$share->max_downloads ?? __('Unlimited')" icon="download" />
|
||||
</div>
|
||||
|
||||
@if ($share->isPasswordProtected())
|
||||
<x-alert color="info" icon="lock" :title="__('This share is password protected')" />
|
||||
@endif
|
||||
|
||||
<div class="flex justify-end">
|
||||
<x-button :label="__('Upload More')" :link="route('upload')" icon="add" variant="tonal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,67 +1,61 @@
|
||||
<div class="w-full max-w-lg mx-auto">
|
||||
<div class="text-center mb-6">
|
||||
{{-- The page a recipient opens. No anchored components (menus, tooltips) on it: it has to work on
|
||||
iOS before Safari 18.4, which cannot position them. --}}
|
||||
|
||||
<div class="mx-auto w-full max-w-lg">
|
||||
<div class="mb-8 text-center">
|
||||
@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" />
|
||||
<img src="{{ Storage::disk('public')->url($siteLogo) }}" alt="{{ $siteTitle ?: config('app.name', 'SealShare') }}" class="mx-auto mb-4 h-20 w-auto" />
|
||||
@endif
|
||||
|
||||
<h1 class="text-2xl font-bold">{{ $siteTitle ?: config('app.name', 'SealShare') }}</h1>
|
||||
<h1 class="type-headline-lg">{{ $siteTitle ?: config('app.name', 'SealShare') }}</h1>
|
||||
|
||||
<p class="mt-2 opacity-70">{{ $siteDescription ?: __('Share your files safely and securely') }}</p>
|
||||
<p class="mt-2 type-body-lg text-on-surface-variant">{{ $siteDescription ?: __('Share your files safely and securely') }}</p>
|
||||
</div>
|
||||
|
||||
@if (! $authenticated)
|
||||
{{-- Password form --}}
|
||||
<form wire:submit="verifyPassword">
|
||||
<x-card title="{{ __('Password Required') }}" subtitle="{{ __('Enter the password to access these files') }}" shadow>
|
||||
<x-card :title="__('Password Required')" :subtitle="__('Enter the password to access these files')" variant="outlined">
|
||||
<x-password
|
||||
wire:model="password"
|
||||
label="{{ __('Password') }}"
|
||||
:label="__('Password')"
|
||||
required
|
||||
placeholder="{{ __('Enter share password') }}"
|
||||
autocomplete="off"
|
||||
:placeholder="__('Enter share password')"
|
||||
/>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button type="submit" label="{{ __('Unlock') }}" class="btn-primary w-full mt-4" icon="o-lock-open" spinner="verifyPassword" />
|
||||
<x-button type="submit" :label="__('Unlock')" variant="filled" icon="lock_open" spinner="verifyPassword" class="w-full" />
|
||||
</x-slot:actions>
|
||||
</x-card>
|
||||
</form>
|
||||
@else
|
||||
{{-- File list --}}
|
||||
<x-card title="{{ __('Shared Files') }}" shadow>
|
||||
<div class="space-y-2 mb-4">
|
||||
<x-card :title="__('Shared Files')" variant="outlined">
|
||||
<x-list :label="__('Shared Files')">
|
||||
@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>
|
||||
<x-list-item
|
||||
:title="$file->relative_path ?: $file->original_name"
|
||||
:description="Number::fileSize($file->file_size)"
|
||||
icon="description"
|
||||
wire:key="file-{{ $file->id }}"
|
||||
>
|
||||
<x-slot:end>
|
||||
<x-button icon="download" :link="route('share.download.file', [$share, $file])" no-wire-navigate :aria-label="__('Download :name', ['name' => $file->original_name])" />
|
||||
</x-slot:end>
|
||||
</x-list-item>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-list>
|
||||
|
||||
@if ($share->expires_at)
|
||||
<p class="text-xs opacity-50 mb-2">
|
||||
<p class="mt-2 type-body-sm text-on-surface-variant">
|
||||
{{ __('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 w-full">
|
||||
<x-icon name="o-arrow-down-tray" class="w-4 h-4" />
|
||||
{{ __('Download All as ZIP') }}
|
||||
</a>
|
||||
<x-button :label="__('Download All as ZIP')" icon="download" variant="filled" :link="route('share.download.all', $share)" no-wire-navigate class="w-full" />
|
||||
@else
|
||||
<a href="{{ route('share.download.file', [$share, $share->files->first()]) }}" class="btn btn-primary w-full">
|
||||
<x-icon name="o-arrow-down-tray" class="w-4 h-4" />
|
||||
{{ __('Download') }}
|
||||
</a>
|
||||
<x-button :label="__('Download')" icon="download" variant="filled" :link="route('share.download.file', [$share, $share->files->first()])" no-wire-navigate class="w-full" />
|
||||
@endif
|
||||
</x-slot:actions>
|
||||
</x-card>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<form wire:submit="verify" class="flex flex-col gap-6">
|
||||
<x-password
|
||||
wire:model="password"
|
||||
label="{{ __('Password') }}"
|
||||
:label="__('Password')"
|
||||
required
|
||||
placeholder="{{ __('System password') }}"
|
||||
:placeholder="__('System password')"
|
||||
/>
|
||||
|
||||
<x-button type="submit" label="{{ __('Continue') }}" class="btn-primary w-full" spinner="verify" />
|
||||
<x-button type="submit" :label="__('Continue')" variant="filled" class="w-full" spinner="verify" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<x-layouts::auth>
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header
|
||||
:title="__('Confirm password')"
|
||||
:description="__('This is a secure area of the application. Please confirm your password before continuing.')"
|
||||
<x-layouts::auth :title="__('Confirm password')">
|
||||
<x-auth-header
|
||||
:title="__('Confirm password')"
|
||||
:description="__('This is a secure area of the application. Please confirm your password before continuing.')"
|
||||
/>
|
||||
|
||||
<x-auth-session-status :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.confirm.store') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
|
||||
<x-password
|
||||
name="password"
|
||||
:label="__('Password')"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.confirm.store') }}" class="flex flex-col gap-6">
|
||||
@csrf
|
||||
|
||||
<x-password
|
||||
name="password"
|
||||
label="{{ __('Password') }}"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
/>
|
||||
|
||||
<x-button type="submit" label="{{ __('Confirm') }}" class="btn-primary w-full" data-test="confirm-password-button" />
|
||||
</form>
|
||||
</div>
|
||||
<x-button type="submit" :label="__('Confirm')" variant="filled" class="w-full" data-test="confirm-password-button" />
|
||||
</form>
|
||||
</x-layouts::auth>
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
<x-layouts::auth>
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Forgot password')" :description="__('Enter your email to receive a password reset link')" />
|
||||
<x-layouts::auth :title="__('Forgot password')">
|
||||
<x-auth-header :title="__('Forgot password')" :description="__('Enter your email to receive a password reset link')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
<x-auth-session-status :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}" class="flex flex-col gap-6">
|
||||
@csrf
|
||||
<form method="POST" action="{{ route('password.email') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<x-input
|
||||
name="email"
|
||||
label="{{ __('Email Address') }}"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
placeholder="email@example.com"
|
||||
icon="o-envelope"
|
||||
/>
|
||||
<x-input
|
||||
name="email"
|
||||
:label="__('Email Address')"
|
||||
:value="old('email')"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
placeholder="email@example.com"
|
||||
icon="mail"
|
||||
/>
|
||||
|
||||
<x-button type="submit" label="{{ __('Email password reset link') }}" class="btn-primary w-full" data-test="email-password-reset-link-button" />
|
||||
</form>
|
||||
<x-button type="submit" :label="__('Email password reset link')" variant="filled" class="w-full" data-test="email-password-reset-link-button" />
|
||||
</form>
|
||||
|
||||
<div class="space-x-1 rtl:space-x-reverse text-center text-sm opacity-60">
|
||||
<span>{{ __('Or, return to') }}</span>
|
||||
<a href="{{ route('login') }}" class="link link-primary" wire:navigate>{{ __('log in') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center type-body-md text-on-surface-variant">
|
||||
{{ __('Or, return to') }}
|
||||
<a href="{{ route('login') }}" class="link" wire:navigate>{{ __('log in') }}</a>
|
||||
</p>
|
||||
</x-layouts::auth>
|
||||
|
||||
@@ -1,59 +1,40 @@
|
||||
<x-layouts::auth>
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Log in to your account')" :description="__('Enter your email and password below to log in')" />
|
||||
<x-layouts::auth :title="__('Log in')">
|
||||
<x-auth-header :title="__('Log in to your account')" :description="__('Enter your email and password below to log in')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
<x-auth-session-status :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('login.store') }}" class="flex flex-col gap-6">
|
||||
@csrf
|
||||
<form method="POST" action="{{ route('login.store') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<x-input
|
||||
name="email"
|
||||
label="{{ __('Email address') }}"
|
||||
:value="old('email')"
|
||||
type="email"
|
||||
<x-input
|
||||
name="email"
|
||||
:label="__('Email address')"
|
||||
:value="old('email')"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
icon="mail"
|
||||
/>
|
||||
|
||||
<div class="grid gap-1">
|
||||
<x-password
|
||||
name="password"
|
||||
:label="__('Password')"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
icon="o-envelope"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="relative">
|
||||
<x-password
|
||||
name="password"
|
||||
label="{{ __('Password') }}"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
/>
|
||||
@if (Route::has('password.request'))
|
||||
<a class="link w-fit justify-self-end type-label-lg" href="{{ route('password.request') }}" wire:navigate>
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (Route::has('password.request'))
|
||||
<a class="absolute top-0 text-sm end-0 link link-primary" href="{{ route('password.request') }}" wire:navigate>
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<x-checkbox name="remember" :label="__('Remember me')" :checked="(bool) old('remember')" />
|
||||
|
||||
<!-- Remember Me -->
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="remember" class="checkbox checkbox-sm" {{ old('remember') ? 'checked' : '' }} />
|
||||
<span class="text-sm">{{ __('Remember me') }}</span>
|
||||
</label>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<x-button type="submit" label="{{ __('Log in') }}" class="btn-primary w-full" data-test="login-button" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (Route::has('register'))
|
||||
<div class="space-x-1 text-sm text-center rtl:space-x-reverse opacity-60">
|
||||
<span>{{ __('Don\'t have an account?') }}</span>
|
||||
<a href="{{ route('register') }}" class="link link-primary" wire:navigate>{{ __('Sign up') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<x-button type="submit" :label="__('Log in')" variant="filled" class="w-full" data-test="login-button" />
|
||||
</form>
|
||||
</x-layouts::auth>
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<x-layouts::auth>
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Create an account')" :description="__('Enter your details below to create your account')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('register.store') }}" class="flex flex-col gap-6">
|
||||
@csrf
|
||||
<!-- Name -->
|
||||
<x-input
|
||||
name="name"
|
||||
label="{{ __('Name') }}"
|
||||
:value="old('name')"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
placeholder="{{ __('Full name') }}"
|
||||
icon="o-user"
|
||||
/>
|
||||
|
||||
<!-- Email Address -->
|
||||
<x-input
|
||||
name="email"
|
||||
label="{{ __('Email address') }}"
|
||||
:value="old('email')"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
icon="o-envelope"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<x-password
|
||||
name="password"
|
||||
label="{{ __('Password') }}"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<x-password
|
||||
name="password_confirmation"
|
||||
label="{{ __('Confirm password') }}"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="{{ __('Confirm password') }}"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<x-button type="submit" label="{{ __('Create account') }}" class="btn-primary w-full" data-test="register-user-button" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="space-x-1 rtl:space-x-reverse text-center text-sm opacity-60">
|
||||
<span>{{ __('Already have an account?') }}</span>
|
||||
<a href="{{ route('login') }}" class="link link-primary" wire:navigate>{{ __('Log in') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts::auth>
|
||||
@@ -1,47 +1,36 @@
|
||||
<x-layouts::auth>
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Reset password')" :description="__('Please enter your new password below')" />
|
||||
<x-layouts::auth :title="__('Reset password')">
|
||||
<x-auth-header :title="__('Reset password')" :description="__('Please enter your new password below')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
<x-auth-session-status :status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.update') }}" class="flex flex-col gap-6">
|
||||
@csrf
|
||||
<!-- Token -->
|
||||
<input type="hidden" name="token" value="{{ request()->route('token') }}">
|
||||
<form method="POST" action="{{ route('password.update') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
<input type="hidden" name="token" value="{{ request()->route('token') }}">
|
||||
|
||||
<!-- Email Address -->
|
||||
<x-input
|
||||
name="email"
|
||||
value="{{ request('email') }}"
|
||||
label="{{ __('Email') }}"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
icon="o-envelope"
|
||||
/>
|
||||
<x-input
|
||||
name="email"
|
||||
:value="old('email', request('email'))"
|
||||
:label="__('Email')"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
icon="mail"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<x-password
|
||||
name="password"
|
||||
label="{{ __('Password') }}"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
/>
|
||||
<x-password
|
||||
name="password"
|
||||
:label="__('Password')"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<x-password
|
||||
name="password_confirmation"
|
||||
label="{{ __('Confirm password') }}"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
placeholder="{{ __('Confirm password') }}"
|
||||
/>
|
||||
<x-password
|
||||
name="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<x-button type="submit" label="{{ __('Reset password') }}" class="btn-primary w-full" data-test="reset-password-button" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<x-button type="submit" :label="__('Reset password')" variant="filled" class="w-full" data-test="reset-password-button" />
|
||||
</form>
|
||||
</x-layouts::auth>
|
||||
|
||||
@@ -1,90 +1,65 @@
|
||||
<x-layouts::auth>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div
|
||||
class="relative w-full h-auto"
|
||||
x-cloak
|
||||
x-data="{
|
||||
showRecoveryInput: @js($errors->has('recovery_code')),
|
||||
code: '',
|
||||
recovery_code: '',
|
||||
toggleInput() {
|
||||
this.showRecoveryInput = !this.showRecoveryInput;
|
||||
this.code = '';
|
||||
this.recovery_code = '';
|
||||
$nextTick(() => {
|
||||
this.showRecoveryInput
|
||||
? this.$refs.recovery_code?.focus()
|
||||
: this.$refs.code?.focus();
|
||||
<x-layouts::auth :title="__('Two-factor authentication')">
|
||||
<div
|
||||
class="flex flex-col gap-6"
|
||||
x-data="{
|
||||
showRecoveryInput: @js($errors->has('recovery_code')),
|
||||
toggleInput() {
|
||||
this.showRecoveryInput = ! this.showRecoveryInput;
|
||||
$nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
(this.showRecoveryInput ? $refs.recovery : $refs.code)?.querySelector('input')?.focus();
|
||||
});
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div x-show="!showRecoveryInput">
|
||||
<x-auth-header
|
||||
:title="__('Authentication Code')"
|
||||
:description="__('Enter the authentication code provided by your authenticator application.')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div x-show="showRecoveryInput">
|
||||
<x-auth-header
|
||||
:title="__('Recovery Code')"
|
||||
:description="__('Please confirm access to your account by entering one of your emergency recovery codes.')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('two-factor.login.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-5 text-center">
|
||||
<div x-show="!showRecoveryInput">
|
||||
<div class="my-5">
|
||||
<x-input
|
||||
type="text"
|
||||
name="code"
|
||||
x-ref="code"
|
||||
x-model="code"
|
||||
x-bind:required="!showRecoveryInput"
|
||||
autocomplete="one-time-code"
|
||||
placeholder="000000"
|
||||
class="text-center text-2xl tracking-widest"
|
||||
maxlength="6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="showRecoveryInput">
|
||||
<div class="my-5">
|
||||
<x-input
|
||||
type="text"
|
||||
name="recovery_code"
|
||||
x-ref="recovery_code"
|
||||
x-bind:required="showRecoveryInput"
|
||||
autocomplete="one-time-code"
|
||||
x-model="recovery_code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@error('recovery_code')
|
||||
<p class="text-error text-sm">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
type="submit"
|
||||
label="{{ __('Continue') }}"
|
||||
class="btn-primary w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 space-x-0.5 text-sm leading-5 text-center">
|
||||
<span class="opacity-50">{{ __('or you can') }}</span>
|
||||
<div class="inline font-medium underline cursor-pointer opacity-80">
|
||||
<span x-show="!showRecoveryInput" @click="toggleInput()">{{ __('login using a recovery code') }}</span>
|
||||
<span x-show="showRecoveryInput" @click="toggleInput()">{{ __('login using an authentication code') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
});
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div x-show="! showRecoveryInput">
|
||||
<x-auth-header
|
||||
:title="__('Authentication Code')"
|
||||
:description="__('Enter the authentication code provided by your authenticator application.')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div x-show="showRecoveryInput" x-cloak>
|
||||
<x-auth-header
|
||||
:title="__('Recovery Code')"
|
||||
:description="__('Please confirm access to your account by entering one of your emergency recovery codes.')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('two-factor.login.store') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
|
||||
<div x-ref="code" x-show="! showRecoveryInput">
|
||||
<x-input
|
||||
name="code"
|
||||
:label="__('Code')"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
maxlength="6"
|
||||
mono
|
||||
autofocus
|
||||
x-bind:disabled="showRecoveryInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div x-ref="recovery" x-show="showRecoveryInput" x-cloak>
|
||||
<x-input
|
||||
name="recovery_code"
|
||||
:label="__('Recovery code')"
|
||||
autocomplete="one-time-code"
|
||||
mono
|
||||
x-bind:disabled="! showRecoveryInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<x-button type="submit" :label="__('Continue')" variant="filled" class="w-full" />
|
||||
</form>
|
||||
|
||||
<p class="text-center type-body-md text-on-surface-variant">
|
||||
{{ __('or you can') }}
|
||||
<button type="button" class="link" x-show="! showRecoveryInput" x-on:click="toggleInput()">{{ __('login using a recovery code') }}</button>
|
||||
<button type="button" class="link" x-show="showRecoveryInput" x-cloak x-on:click="toggleInput()">{{ __('login using an authentication code') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</x-layouts::auth>
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
<x-layouts::auth>
|
||||
<div class="mt-4 flex flex-col gap-6">
|
||||
<p class="text-center text-sm opacity-70">
|
||||
{{ __('Please verify your email address by clicking on the link we just emailed to you.') }}
|
||||
</p>
|
||||
<x-layouts::auth :title="__('Verify email')">
|
||||
<x-auth-header
|
||||
:title="__('Verify your email')"
|
||||
:description="__('Please verify your email address by clicking on the link we just emailed to you.')"
|
||||
/>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<p class="text-center text-sm font-medium text-success">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</p>
|
||||
@endif
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<x-alert color="success">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</x-alert>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col items-center justify-between space-y-3">
|
||||
<form method="POST" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
<x-button type="submit" label="{{ __('Resend verification email') }}" class="btn-primary w-full" />
|
||||
</form>
|
||||
<div class="flex flex-col items-stretch gap-3">
|
||||
<form method="POST" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
<x-button type="submit" :label="__('Resend verification email')" variant="filled" class="w-full" />
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<x-button type="submit" label="{{ __('Log out') }}" class="btn-ghost btn-sm" data-test="logout-button" />
|
||||
</form>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('logout') }}" class="self-center">
|
||||
@csrf
|
||||
<x-button type="submit" :label="__('Log out')" data-test="logout-button" />
|
||||
</form>
|
||||
</div>
|
||||
</x-layouts::auth>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
<div class="flex items-start max-md:flex-col">
|
||||
<div class="me-10 w-full pb-4 md:w-[220px]">
|
||||
<x-menu class="!p-0">
|
||||
<x-menu-item title="{{ __('Profile') }}" link="{{ route('profile.edit') }}" wire:navigate />
|
||||
<x-menu-item title="{{ __('Password') }}" link="{{ route('user-password.edit') }}" wire:navigate />
|
||||
@if (Laravel\Fortify\Features::canManageTwoFactorAuthentication())
|
||||
<x-menu-item title="{{ __('Two-Factor Auth') }}" link="{{ route('two-factor.show') }}" wire:navigate />
|
||||
@endif
|
||||
<x-menu-item title="{{ __('Appearance') }}" link="{{ route('appearance.edit') }}" wire:navigate />
|
||||
</x-menu>
|
||||
</div>
|
||||
@php
|
||||
$items = [
|
||||
['title' => __('Profile'), 'icon' => 'person', 'url' => route('profile.edit'), 'active' => request()->routeIs('profile.edit')],
|
||||
['title' => __('Password'), 'icon' => 'password', 'url' => route('user-password.edit'), 'active' => request()->routeIs('user-password.edit')],
|
||||
];
|
||||
|
||||
<div class="divider md:hidden"></div>
|
||||
if (Laravel\Fortify\Features::canManageTwoFactorAuthentication()) {
|
||||
$items[] = ['title' => __('Two-Factor Auth'), 'icon' => 'shield_lock', 'url' => route('two-factor.show'), 'active' => request()->routeIs('two-factor.show')];
|
||||
}
|
||||
|
||||
<div class="flex-1 self-stretch max-md:pt-6">
|
||||
<h2 class="text-lg font-semibold">{{ $heading ?? '' }}</h2>
|
||||
<p class="text-sm opacity-60">{{ $subheading ?? '' }}</p>
|
||||
$items[] = ['title' => __('Appearance'), 'icon' => 'contrast', 'url' => route('appearance.edit'), 'active' => request()->routeIs('appearance.edit')];
|
||||
@endphp
|
||||
|
||||
<div class="mt-5 w-full max-w-lg">
|
||||
<div class="w-full">
|
||||
<x-section-nav :items="$items" :label="__('Settings')" />
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="type-title-lg">{{ $heading ?? '' }}</h2>
|
||||
<p class="mt-1 type-body-md text-on-surface-variant">{{ $subheading ?? '' }}</p>
|
||||
|
||||
<div class="mt-6 w-full max-w-lg">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,83 +45,48 @@ new class extends Component {
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div
|
||||
class="py-6 space-y-6 border shadow-sm rounded-xl border-base-300"
|
||||
wire:cloak
|
||||
x-data="{ showRecoveryCodes: false }"
|
||||
>
|
||||
<div class="px-6 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-icon name="o-lock-closed" class="w-4 h-4" />
|
||||
<h3 class="text-lg font-semibold">{{ __('2FA Recovery Codes') }}</h3>
|
||||
<x-card variant="outlined" wire:cloak x-data="{ showRecoveryCodes: false }">
|
||||
<div class="grid gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<x-icon name="lock" class="size-5 text-on-surface-variant" />
|
||||
<h3 class="type-title-md">{{ __('2FA Recovery Codes') }}</h3>
|
||||
</div>
|
||||
<p class="mt-1 type-body-md text-on-surface-variant">
|
||||
{{ __('Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm opacity-60">
|
||||
{{ __('Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="px-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<x-button
|
||||
x-show="!showRecoveryCodes"
|
||||
icon="o-eye"
|
||||
label="{{ __('View Recovery Codes') }}"
|
||||
class="btn-primary btn-sm"
|
||||
@click="showRecoveryCodes = true;"
|
||||
/>
|
||||
|
||||
<x-button
|
||||
x-show="showRecoveryCodes"
|
||||
icon="o-eye-slash"
|
||||
label="{{ __('Hide Recovery Codes') }}"
|
||||
class="btn-primary btn-sm"
|
||||
@click="showRecoveryCodes = false"
|
||||
/>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span x-show="! showRecoveryCodes" class="inline-flex">
|
||||
<x-button icon="visibility" :label="__('View Recovery Codes')" variant="tonal" x-on:click="showRecoveryCodes = true" />
|
||||
</span>
|
||||
<span x-show="showRecoveryCodes" x-cloak class="inline-flex">
|
||||
<x-button icon="visibility_off" :label="__('Hide Recovery Codes')" variant="tonal" x-on:click="showRecoveryCodes = false" />
|
||||
</span>
|
||||
|
||||
@if (filled($recoveryCodes))
|
||||
<x-button
|
||||
x-show="showRecoveryCodes"
|
||||
icon="o-arrow-path"
|
||||
label="{{ __('Regenerate Codes') }}"
|
||||
class="btn-sm"
|
||||
wire:click="regenerateRecoveryCodes"
|
||||
/>
|
||||
<span x-show="showRecoveryCodes" x-cloak class="inline-flex">
|
||||
<x-button icon="refresh" :label="__('Regenerate Codes')" variant="outlined" wire:click="regenerateRecoveryCodes" />
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-show="showRecoveryCodes"
|
||||
x-transition
|
||||
id="recovery-codes-section"
|
||||
class="relative overflow-hidden"
|
||||
x-bind:aria-hidden="!showRecoveryCodes"
|
||||
>
|
||||
<div class="mt-3 space-y-3">
|
||||
@error('recoveryCodes')
|
||||
<div class="alert alert-error">{{ $message }}</div>
|
||||
@enderror
|
||||
<div x-show="showRecoveryCodes" x-cloak id="recovery-codes-section" class="grid gap-3">
|
||||
@error('recoveryCodes')
|
||||
<x-alert color="error">{{ $message }}</x-alert>
|
||||
@enderror
|
||||
|
||||
@if (filled($recoveryCodes))
|
||||
<div
|
||||
class="grid gap-1 p-4 font-mono text-sm rounded-lg bg-base-200"
|
||||
role="list"
|
||||
aria-label="{{ __('Recovery codes') }}"
|
||||
>
|
||||
@foreach($recoveryCodes as $code)
|
||||
<div
|
||||
role="listitem"
|
||||
class="select-text"
|
||||
wire:loading.class="opacity-50 animate-pulse"
|
||||
>
|
||||
{{ $code }}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<p class="text-xs opacity-60">
|
||||
{{ __('Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate Codes above.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@if (filled($recoveryCodes))
|
||||
<div class="grid gap-1 rounded-corner-md bg-surface-container-highest p-4 font-mono type-body-md" role="list" aria-label="{{ __('Recovery codes') }}">
|
||||
@foreach ($recoveryCodes as $code)
|
||||
<div role="listitem" class="select-text" wire:loading.class="animate-pulse opacity-50">{{ $code }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<p class="type-body-sm text-on-surface-variant">
|
||||
{{ __('Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate Codes above.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-card>
|
||||
|
||||
@@ -10,6 +10,6 @@ new class extends Component {
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-pages::settings.layout :heading="__('Appearance')" :subheading="__('Update the appearance settings for your account')">
|
||||
<x-theme-toggle />
|
||||
<x-theme-toggle mode="picker" class="w-full max-w-sm" data-test="appearance-picker" />
|
||||
</x-pages::settings.layout>
|
||||
</section>
|
||||
|
||||
@@ -26,31 +26,30 @@ new class extends Component {
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<section class="mt-10 space-y-6">
|
||||
<div class="relative mb-5">
|
||||
<h3 class="text-lg font-semibold">{{ __('Delete account') }}</h3>
|
||||
<p class="text-sm opacity-60">{{ __('Delete your account and all of its resources') }}</p>
|
||||
<section class="mt-12 grid gap-4">
|
||||
<x-divider />
|
||||
|
||||
<div>
|
||||
<h3 class="type-title-md">{{ __('Delete account') }}</h3>
|
||||
<p class="mt-1 type-body-md text-on-surface-variant">{{ __('Delete your account and all of its resources') }}</p>
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
label="{{ __('Delete account') }}"
|
||||
class="btn-error"
|
||||
@click="$wire.showDeleteModal = true"
|
||||
data-test="delete-user-button"
|
||||
/>
|
||||
<div>
|
||||
<x-button :label="__('Delete account')" danger icon="delete" wire:click="$set('showDeleteModal', true)" data-test="delete-user-button" />
|
||||
</div>
|
||||
|
||||
<x-modal wire:model="showDeleteModal" title="{{ __('Are you sure you want to delete your account?') }}">
|
||||
<p class="text-sm opacity-70">
|
||||
<x-modal wire:model="showDeleteModal" :title="__('Are you sure you want to delete your account?')" icon="delete">
|
||||
<p>
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
|
||||
</p>
|
||||
|
||||
<form wire:submit="deleteUser" class="space-y-6 mt-4">
|
||||
<x-password wire:model="password" label="{{ __('Password') }}" />
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button label="{{ __('Cancel') }}" @click="$wire.showDeleteModal = false" />
|
||||
<x-button type="submit" label="{{ __('Delete account') }}" class="btn-error" data-test="confirm-delete-user-button" />
|
||||
</x-slot:actions>
|
||||
<form id="delete-user-form" wire:submit="deleteUser" class="mt-4">
|
||||
<x-password wire:model="password" :label="__('Password')" autocomplete="current-password" />
|
||||
</form>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button :label="__('Cancel')" x-on:click="close()" />
|
||||
<x-button type="submit" form="delete-user-form" :label="__('Delete account')" danger data-test="confirm-delete-user-button" />
|
||||
</x-slot:actions>
|
||||
</x-modal>
|
||||
</section>
|
||||
|
||||
@@ -5,9 +5,11 @@ use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Component;
|
||||
use NoNameWeb\LivewireMaterial\Concerns\Toasts;
|
||||
|
||||
new class extends Component {
|
||||
use PasswordValidationRules;
|
||||
use Toasts;
|
||||
|
||||
public string $current_password = '';
|
||||
public string $password = '';
|
||||
@@ -36,6 +38,8 @@ new class extends Component {
|
||||
$this->reset('current_password', 'password', 'password_confirmation');
|
||||
|
||||
$this->dispatch('password-updated');
|
||||
|
||||
$this->success(__('Saved.'));
|
||||
}
|
||||
}; ?>
|
||||
|
||||
@@ -43,34 +47,13 @@ new class extends Component {
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-pages::settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
|
||||
<form method="POST" wire:submit="updatePassword" class="mt-6 space-y-6">
|
||||
<x-password
|
||||
wire:model="current_password"
|
||||
label="{{ __('Current password') }}"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<x-password
|
||||
wire:model="password"
|
||||
label="{{ __('New password') }}"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<x-password
|
||||
wire:model="password_confirmation"
|
||||
label="{{ __('Confirm Password') }}"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<form method="POST" wire:submit="updatePassword" class="grid gap-5">
|
||||
<x-password wire:model="current_password" :label="__('Current password')" required autocomplete="current-password" />
|
||||
<x-password wire:model="password" :label="__('New password')" required autocomplete="new-password" />
|
||||
<x-password wire:model="password_confirmation" :label="__('Confirm Password')" required autocomplete="new-password" />
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<x-button type="submit" label="{{ __('Save') }}" class="btn-primary" spinner="updatePassword" data-test="update-password-button" />
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="password-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
<div>
|
||||
<x-button type="submit" :label="__('Save')" variant="filled" spinner="updatePassword" data-test="update-password-button" />
|
||||
</div>
|
||||
</form>
|
||||
</x-pages::settings.layout>
|
||||
|
||||
@@ -8,9 +8,11 @@ use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
use NoNameWeb\LivewireMaterial\Concerns\Toasts;
|
||||
|
||||
new class extends Component {
|
||||
use ProfileValidationRules;
|
||||
use Toasts;
|
||||
|
||||
public string $name = '';
|
||||
public string $email = '';
|
||||
@@ -42,6 +44,8 @@ new class extends Component {
|
||||
$user->save();
|
||||
|
||||
$this->dispatch('profile-updated', name: $user->name);
|
||||
|
||||
$this->success(__('Saved.'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +56,7 @@ new class extends Component {
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false));
|
||||
$this->redirectIntended(default: route('admin.dashboard', absolute: false));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -80,39 +84,29 @@ new class extends Component {
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-pages::settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
|
||||
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
|
||||
<x-input wire:model="name" label="{{ __('Name') }}" type="text" required autofocus autocomplete="name" icon="o-user" />
|
||||
<form wire:submit="updateProfileInformation" class="grid w-full gap-5">
|
||||
<x-input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" icon="person" />
|
||||
|
||||
<div>
|
||||
<x-input wire:model="email" label="{{ __('Email') }}" type="email" required autocomplete="email" icon="o-envelope" />
|
||||
<div class="grid gap-3">
|
||||
<x-input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" icon="mail" />
|
||||
|
||||
@if ($this->hasUnverifiedEmail)
|
||||
<div>
|
||||
<p class="mt-4 text-sm opacity-70">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
<p class="type-body-md text-on-surface-variant">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<a class="link link-primary text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</a>
|
||||
</p>
|
||||
<button type="button" class="link" wire:click.prevent="resendVerificationNotification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<p class="mt-2 text-sm font-medium text-success">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<x-alert color="success">{{ __('A new verification link has been sent to your email address.') }}</x-alert>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<x-button type="submit" label="{{ __('Save') }}" class="btn-primary" spinner="updateProfileInformation" data-test="update-profile-button" />
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="profile-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
<div>
|
||||
<x-button type="submit" :label="__('Save')" variant="filled" spinner="updateProfileInformation" data-test="update-profile-button" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -184,142 +184,81 @@ new class extends Component {
|
||||
:heading="__('Two Factor Authentication')"
|
||||
:subheading="__('Manage your two-factor authentication settings')"
|
||||
>
|
||||
<div class="flex flex-col w-full mx-auto space-y-6 text-sm" wire:cloak>
|
||||
<div class="grid w-full gap-6" wire:cloak>
|
||||
@if ($twoFactorEnabled)
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="badge badge-success">{{ __('Enabled') }}</span>
|
||||
</div>
|
||||
<div class="grid justify-items-start gap-4">
|
||||
<x-badge :value="__('Enabled')" tonal color="success" />
|
||||
|
||||
<p class="opacity-70">
|
||||
<p class="type-body-md text-on-surface-variant">
|
||||
{{ __('With two-factor authentication enabled, you will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<livewire:pages::settings.two-factor.recovery-codes :$requiresConfirmation />
|
||||
<livewire:pages::settings.two-factor.recovery-codes :$requiresConfirmation />
|
||||
|
||||
<div class="flex justify-start">
|
||||
<x-button
|
||||
label="{{ __('Disable 2FA') }}"
|
||||
icon="o-shield-exclamation"
|
||||
class="btn-error"
|
||||
wire:click="disable"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<x-button :label="__('Disable 2FA')" icon="remove_moderator" danger wire:click="disable" />
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="badge badge-error">{{ __('Disabled') }}</span>
|
||||
</div>
|
||||
<div class="grid justify-items-start gap-4">
|
||||
<x-badge :value="__('Disabled')" tonal color="error" />
|
||||
|
||||
<p class="opacity-60">
|
||||
<p class="type-body-md text-on-surface-variant">
|
||||
{{ __('When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.') }}
|
||||
</p>
|
||||
|
||||
<x-button
|
||||
label="{{ __('Enable 2FA') }}"
|
||||
icon="o-shield-check"
|
||||
class="btn-primary"
|
||||
wire:click="enable"
|
||||
/>
|
||||
<x-button :label="__('Enable 2FA')" icon="shield_lock" variant="filled" wire:click="enable" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-pages::settings.layout>
|
||||
|
||||
<x-modal wire:model="showModal" :title="$this->modalConfig['title']" class="max-w-md">
|
||||
<p class="text-sm opacity-70">{{ $this->modalConfig['description'] }}</p>
|
||||
|
||||
<x-modal wire:model="showModal" :title="$this->modalConfig['title']" :subtitle="$this->modalConfig['description']" fullscreen>
|
||||
@if ($showVerificationStep)
|
||||
<div class="space-y-6 mt-4">
|
||||
<div class="flex flex-col items-center space-y-3 justify-center">
|
||||
<x-input
|
||||
name="code"
|
||||
wire:model="code"
|
||||
placeholder="000000"
|
||||
class="text-center text-2xl tracking-widest"
|
||||
maxlength="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button
|
||||
label="{{ __('Back') }}"
|
||||
wire:click="resetVerification"
|
||||
/>
|
||||
<x-button
|
||||
label="{{ __('Confirm') }}"
|
||||
class="btn-primary"
|
||||
wire:click="confirmTwoFactor"
|
||||
x-bind:disabled="$wire.code.length < 6"
|
||||
/>
|
||||
</x-slot:actions>
|
||||
<div class="mt-2">
|
||||
<x-input
|
||||
name="code"
|
||||
wire:model="code"
|
||||
:label="__('Code')"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
maxlength="6"
|
||||
mono
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button :label="__('Back')" wire:click="resetVerification" />
|
||||
<x-button :label="__('Confirm')" variant="filled" wire:click="confirmTwoFactor" x-bind:disabled="$wire.code.length < 6" />
|
||||
</x-slot:actions>
|
||||
@else
|
||||
@error('setupData')
|
||||
<div class="alert alert-error mt-4">{{ $message }}</div>
|
||||
<x-alert color="error" class="mt-2">{{ $message }}</x-alert>
|
||||
@enderror
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<div class="relative w-64 overflow-hidden border rounded-lg border-base-300 aspect-square">
|
||||
<div class="mt-2 flex justify-center">
|
||||
{{-- The QR code keeps a white ground in both themes: scanners read dark on light. --}}
|
||||
<div class="grid aspect-square w-64 place-items-center overflow-hidden rounded-corner-lg bg-white p-4">
|
||||
@empty($qrCodeSvg)
|
||||
<div class="absolute inset-0 flex items-center justify-center bg-base-200 animate-pulse">
|
||||
<span class="loading loading-spinner"></span>
|
||||
</div>
|
||||
<x-loading :label="__('Loading')" />
|
||||
@else
|
||||
<div class="flex items-center justify-center h-full p-4 bg-white">
|
||||
{!! $qrCodeSvg !!}
|
||||
</div>
|
||||
{!! $qrCodeSvg !!}
|
||||
@endempty
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<div class="relative flex items-center justify-center w-full">
|
||||
<div class="absolute inset-0 w-full h-px top-1/2 bg-base-300"></div>
|
||||
<span class="relative px-2 text-sm bg-base-100 opacity-60">
|
||||
{{ __('or, enter the code manually') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-6 grid gap-3">
|
||||
<p class="text-center type-label-lg text-on-surface-variant">{{ __('or, enter the code manually') }}</p>
|
||||
|
||||
<div
|
||||
class="flex items-center space-x-2"
|
||||
x-data="{
|
||||
copied: false,
|
||||
async copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText('{{ $manualSetupKey }}');
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 1500);
|
||||
} catch (e) {
|
||||
console.warn('Could not copy to clipboard');
|
||||
}
|
||||
}
|
||||
}"
|
||||
>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value="{{ $manualSetupKey }}"
|
||||
class="input input-bordered join-item w-full"
|
||||
/>
|
||||
<button
|
||||
@click="copy()"
|
||||
class="btn join-item"
|
||||
>
|
||||
<x-icon x-show="!copied" name="o-document-duplicate" class="w-4 h-4" />
|
||||
<x-icon x-show="copied" name="o-check" class="w-4 h-4 text-success" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<x-input :label="__('Setup key')" :value="$manualSetupKey" readonly copyable mono />
|
||||
</div>
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button
|
||||
:disabled="$errors->has('setupData')"
|
||||
label="{{ $this->modalConfig['buttonText'] }}"
|
||||
class="btn-primary"
|
||||
:label="$this->modalConfig['buttonText']"
|
||||
variant="filled"
|
||||
wire:click="showVerificationIfNecessary"
|
||||
/>
|
||||
</x-slot:actions>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{{-- The one top app bar on every page: the site's logo and title, then the account menu for a
|
||||
signed-in user, or the theme toggle and a way to sign in for everyone else. --}}
|
||||
|
||||
@php
|
||||
$siteTitle = \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare');
|
||||
$siteLogo = \App\Models\Setting::get('site_logo');
|
||||
@endphp
|
||||
|
||||
<x-app-bar>
|
||||
<x-slot:navigation>
|
||||
<a href="{{ route('home') }}" class="focus-ring flex min-w-0 items-center gap-3 rounded-corner-full py-2 ps-3 pe-4" data-test="app-bar-home">
|
||||
@if ($siteLogo)
|
||||
<img src="{{ Storage::disk('public')->url($siteLogo) }}" alt="" class="h-8 w-auto" />
|
||||
@else
|
||||
<x-app-logo-icon class="size-8 shrink-0 text-primary" />
|
||||
@endif
|
||||
<span class="truncate type-title-lg text-on-surface">{{ $siteTitle }}</span>
|
||||
</a>
|
||||
</x-slot:navigation>
|
||||
|
||||
<x-slot:actions>
|
||||
@auth
|
||||
<span class="me-2 inline-flex">
|
||||
<x-account-menu :name="auth()->user()->name" :email="auth()->user()->email">
|
||||
<x-menu-item :label="__('Upload')" icon="upload" :link="route('upload')" />
|
||||
@if (auth()->user()->is_admin)
|
||||
<x-menu-item :label="__('Admin dashboard')" icon="dashboard" :link="route('admin.dashboard')" />
|
||||
<x-menu-item :label="__('Admin settings')" icon="admin_panel_settings" :link="route('admin.settings')" />
|
||||
@endif
|
||||
<x-menu-item :label="__('Settings')" icon="settings" :link="route('profile.edit')" />
|
||||
|
||||
<x-slot:footer>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<x-menu-item :label="__('Log out')" icon="logout" type="submit" data-test="logout-button" />
|
||||
</form>
|
||||
</x-slot:footer>
|
||||
</x-account-menu>
|
||||
</span>
|
||||
@else
|
||||
<x-theme-toggle />
|
||||
@if (Route::has('login') && ! request()->routeIs('login'))
|
||||
<span class="me-1 inline-flex"><x-button :label="__('Log in')" :link="route('login')" /></span>
|
||||
@endif
|
||||
@endauth
|
||||
</x-slot:actions>
|
||||
</x-app-bar>
|
||||
@@ -1,5 +1,5 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
|
||||
<title>{{ $title ?? (\App\Models\Setting::get('site_title') ?: config('app.name')) }}</title>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
|
||||
<x-theme-script />
|
||||
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<div class="relative mb-6 w-full">
|
||||
<h1 class="text-2xl font-bold">{{ __('Settings') }}</h1>
|
||||
<p class="text-sm opacity-60 mb-6">{{ __('Manage your profile and account settings') }}</p>
|
||||
<div class="divider my-0"></div>
|
||||
<div class="mb-6 w-full">
|
||||
<h1 class="type-headline-md">{{ __('Settings') }}</h1>
|
||||
<p class="mt-1 type-body-md text-on-surface-variant">{{ __('Manage your profile and account settings') }}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="dark">
|
||||
<script>document.documentElement.setAttribute('data-theme', localStorage.getItem('mary-theme')?.replaceAll('"','') || 'dark')</script>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare') }}</title>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-200 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold mb-2">{{ \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare') }}</h1>
|
||||
<p class="text-base-content/60 mb-6">{{ __('Secure file sharing made simple.') }}</p>
|
||||
<a href="{{ route('upload') }}" class="btn btn-primary">{{ __('Upload Files') }}</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user