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:
surtic86
2026-02-13 11:43:20 +01:00
co-authored by Claude Opus 4.6
parent 4790be8142
commit e37b322dde
120 changed files with 6346 additions and 1459 deletions
+4 -60
View File
@@ -1,66 +1,10 @@
@import 'tailwindcss';
@import '../../vendor/livewire/flux/dist/flux.css';
@source '../views';
@source '../../vendor/robsontenorio/mary/src/View/Components/**/*.php';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
@source '../../vendor/livewire/flux/stubs/**/*.blade.php';
@source inline("swap swap-rotate swap-on swap-off theme-controller");
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--color-zinc-50: #fafafa;
--color-zinc-100: #f5f5f5;
--color-zinc-200: #e5e5e5;
--color-zinc-300: #d4d4d4;
--color-zinc-400: #a3a3a3;
--color-zinc-500: #737373;
--color-zinc-600: #525252;
--color-zinc-700: #404040;
--color-zinc-800: #262626;
--color-zinc-900: #171717;
--color-zinc-950: #0a0a0a;
--color-accent: var(--color-neutral-800);
--color-accent-content: var(--color-neutral-800);
--color-accent-foreground: var(--color-white);
@plugin "daisyui" {
themes: light --default, dark --prefersdark;
}
@layer theme {
.dark {
--color-accent: var(--color-white);
--color-accent-content: var(--color-white);
--color-accent-foreground: var(--color-neutral-800);
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
[data-flux-field]:not(ui-radio, ui-checkbox) {
@apply grid gap-2;
}
[data-flux-label] {
@apply !mb-0 !leading-tight;
}
input:focus[data-flux-control],
textarea:focus[data-flux-control],
select:focus[data-flux-control] {
@apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground;
}
/* \[:where(&)\]:size-4 {
@apply size-4;
} */
+2
View File
@@ -0,0 +1,2 @@
// Alpine.js is bundled and started automatically by Livewire 4.
// Do not import it here to avoid "multiple instances of Alpine" errors.
@@ -1,8 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {{ $attributes }}>
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" {{ $attributes }}>
{{-- Document outline with folded corner --}}
<path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5" />
<path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5" />
{{-- Upload arrow --}}
<path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 517 B

+4 -17
View File
@@ -1,17 +1,4 @@
@props([
'sidebar' => false,
])
@if($sidebar)
<flux:sidebar.brand name="Laravel Starter Kit" {{ $attributes }}>
<x-slot name="logo" class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
</x-slot>
</flux:sidebar.brand>
@else
<flux:brand name="Laravel Starter Kit" {{ $attributes }}>
<x-slot name="logo" class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
</x-slot>
</flux:brand>
@endif
<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">
<flux:heading size="xl">{{ $title }}</flux:heading>
<flux:subheading>{{ $description }}</flux:subheading>
<h2 class="text-xl font-bold">{{ $title }}</h2>
<p class="text-sm opacity-60 mt-1">{{ $description }}</p>
</div>
@@ -1,39 +1 @@
<flux:dropdown position="bottom" align="start">
<flux:sidebar.profile
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
icon:trailing="chevrons-up-down"
data-test="sidebar-menu-button"
/>
<flux:menu>
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<flux:avatar
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
/>
<div class="grid flex-1 text-start text-sm leading-tight">
<flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
<flux:text class="truncate">{{ auth()->user()->email }}</flux:text>
</div>
</div>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
{{ __('Settings') }}
</flux:menu.item>
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item
as="button"
type="submit"
icon="arrow-right-start-on-rectangle"
class="w-full cursor-pointer"
data-test="logout-button"
>
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu.radio.group>
</flux:menu>
</flux:dropdown>
{{-- Desktop user menu - integrated into sidebar layout --}}
+8 -8
View File
@@ -1,18 +1,18 @@
<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-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
<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-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
<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-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
<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-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
<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,47 +0,0 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')->add(
match ($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
},
);
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<path d="M12 7v14" />
<path d="M16 12h2" />
<path d="M16 8h2" />
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
<path d="M6 12h2" />
<path d="M6 8h2" />
</svg>
@@ -1,43 +0,0 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')->add(
match ($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
},
);
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<path d="m7 15 5 5 5-5" />
<path d="m7 9 5-5 5 5" />
</svg>
@@ -1,45 +0,0 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')->add(
match ($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
},
);
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5" />
<circle cx="13" cy="12" r="2" />
<path d="M18 19c-2.8 0-5-2.2-5-5v8" />
<circle cx="20" cy="19" r="2" />
</svg>
@@ -1,45 +0,0 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')->add(
match ($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
},
);
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<rect width="7" height="7" x="3" y="3" rx="1" />
<rect width="7" height="7" x="14" y="3" rx="1" />
<rect width="7" height="7" x="14" y="14" rx="1" />
<rect width="7" height="7" x="3" y="14" rx="1" />
</svg>
@@ -1,51 +0,0 @@
@props([
'expandable' => false,
'expanded' => true,
'heading' => null,
])
<?php if ($expandable && $heading): ?>
<ui-disclosure
{{ $attributes->class('group/disclosure') }}
@if ($expanded === true) open @endif
data-flux-navlist-group
>
<button
type="button"
class="group/disclosure-button mb-[2px] flex h-10 w-full items-center rounded-lg text-zinc-500 hover:bg-zinc-800/5 hover:text-zinc-800 lg:h-8 dark:text-white/80 dark:hover:bg-white/[7%] dark:hover:text-white"
>
<div class="ps-3 pe-4">
<flux:icon.chevron-down class="hidden size-3! group-data-open/disclosure-button:block" />
<flux:icon.chevron-right class="block size-3! group-data-open/disclosure-button:hidden" />
</div>
<span class="text-sm font-medium leading-none">{{ $heading }}</span>
</button>
<div class="relative hidden space-y-[2px] ps-7 data-open:block" @if ($expanded === true) data-open @endif>
<div class="absolute inset-y-[3px] start-0 ms-4 w-px bg-zinc-200 dark:bg-white/30"></div>
{{ $slot }}
</div>
</ui-disclosure>
<?php elseif ($heading): ?>
<div {{ $attributes->class('block space-y-[2px]') }}>
<div class="px-1 py-2">
<div class="text-xs leading-none text-zinc-400">{{ $heading }}</div>
</div>
<div>
{{ $slot }}
</div>
</div>
<?php else: ?>
<div {{ $attributes->class('block space-y-[2px]') }}>
{{ $slot }}
</div>
<?php endif; ?>
+1 -3
View File
@@ -1,5 +1,3 @@
<x-layouts::app.sidebar :title="$title ?? null">
<flux:main>
{{ $slot }}
</flux:main>
{{ $slot }}
</x-layouts::app.sidebar>
+4 -78
View File
@@ -1,78 +1,4 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden mr-2" icon="bars-2" inset="left" />
<x-app-logo href="{{ route('dashboard') }}" wire:navigate />
<flux:navbar class="-mb-px max-lg:hidden">
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:navbar.item>
</flux:navbar>
<flux:spacer />
<flux:navbar class="me-1.5 space-x-0.5 rtl:space-x-reverse py-0!">
<flux:tooltip :content="__('Search')" position="bottom">
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#" :label="__('Search')" />
</flux:tooltip>
<flux:tooltip :content="__('Repository')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="folder-git-2"
href="https://github.com/laravel/livewire-starter-kit"
target="_blank"
:label="__('Repository')"
/>
</flux:tooltip>
<flux:tooltip :content="__('Documentation')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="book-open-text"
href="https://laravel.com/docs/starter-kits#livewire"
target="_blank"
:label="__('Documentation')"
/>
</flux:tooltip>
</flux:navbar>
<x-desktop-user-menu />
</flux:header>
<!-- Mobile Menu -->
<flux:sidebar collapsible="mobile" sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.header>
<x-app-logo :sidebar="true" href="{{ route('dashboard') }}" wire:navigate />
<flux:sidebar.collapse class="in-data-flux-sidebar-on-desktop:not-in-data-flux-sidebar-collapsed-desktop:-mr-2" />
</flux:sidebar.header>
<flux:sidebar.nav>
<flux:sidebar.group :heading="__('Platform')">
<flux:sidebar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:sidebar.item>
</flux:sidebar.group>
</flux:sidebar.nav>
<flux:spacer />
<flux:sidebar.nav>
<flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:sidebar.item>
<flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:sidebar.item>
</flux:sidebar.nav>
</flux:sidebar>
{{ $slot }}
@fluxScripts
</body>
</html>
{{-- Header layout not used - redirects to sidebar layout --}}
<x-layouts::app.sidebar :title="$title ?? null">
{{ $slot }}
</x-layouts::app.sidebar>
+33 -87
View File
@@ -1,96 +1,42 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<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 bg-white dark:bg-zinc-800">
<flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.header>
<x-app-logo :sidebar="true" href="{{ route('dashboard') }}" wire:navigate />
<flux:sidebar.collapse class="lg:hidden" />
</flux:sidebar.header>
<body class="min-h-screen font-sans antialiased bg-base-200/50 flex flex-col">
<flux:sidebar.nav>
<flux:sidebar.group :heading="__('Platform')" class="grid">
<flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:sidebar.item>
</flux:sidebar.group>
</flux:sidebar.nav>
{{-- MAIN CONTENT --}}
<main class="flex-1 w-full max-w-5xl mx-auto px-4 py-8">
{{ $slot }}
</main>
<flux:spacer />
{{-- 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>
<flux:sidebar.nav>
<flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:sidebar.item>
<flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:sidebar.item>
</flux:sidebar.nav>
<x-desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" />
</flux:sidebar>
<!-- Mobile User Menu -->
<flux:header class="lg:hidden">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<flux:spacer />
<flux:dropdown position="top" align="end">
<flux:profile
:initials="auth()->user()->initials()"
icon-trailing="chevron-down"
/>
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<flux:avatar
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
/>
<div class="grid flex-1 text-start text-sm leading-tight">
<flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
<flux:text class="truncate">{{ auth()->user()->email }}</flux:text>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
{{ __('Settings') }}
</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item
as="button"
type="submit"
icon="arrow-right-start-on-rectangle"
class="w-full cursor-pointer"
data-test="logout-button"
>
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
{{ $slot }}
@fluxScripts
{{-- Toast --}}
<x-toast />
</body>
</html>
+4 -26
View File
@@ -1,26 +1,4 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-md flex-col gap-6">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8">{{ $slot }}</div>
</div>
</div>
</div>
</div>
@fluxScripts
</body>
</html>
{{-- Card auth layout - delegates to simple layout --}}
<x-layouts::auth.simple :title="$title ?? null">
{{ $slot }}
</x-layouts::auth.simple>
+11 -6
View File
@@ -1,22 +1,27 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<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-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<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 text-black dark:text-white" />
<x-app-logo-icon class="size-9 fill-current" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</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>
@fluxScripts
@livewireScripts
</body>
</html>
+4 -43
View File
@@ -1,43 +1,4 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
<div class="absolute inset-0 bg-neutral-900"></div>
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
<span class="flex h-10 w-10 items-center justify-center rounded-md">
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
</span>
{{ config('app.name', 'Laravel') }}
</a>
@php
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
@endphp
<div class="relative z-20 mt-auto">
<blockquote class="space-y-2">
<flux:heading size="lg">&ldquo;{{ trim($message) }}&rdquo;</flux:heading>
<footer><flux:heading>{{ trim($author) }}</flux:heading></footer>
</blockquote>
</div>
</div>
<div class="w-full lg:p-8">
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
{{ $slot }}
</div>
</div>
</div>
@fluxScripts
</body>
</html>
{{-- Split auth layout - delegates to simple layout --}}
<x-layouts::auth.simple :title="$title ?? null">
{{ $slot }}
</x-layouts::auth.simple>
@@ -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>
@@ -10,19 +10,15 @@
<form method="POST" action="{{ route('password.confirm.store') }}" class="flex flex-col gap-6">
@csrf
<flux:input
<x-password
name="password"
:label="__('Password')"
type="password"
label="{{ __('Password') }}"
required
autocomplete="current-password"
:placeholder="__('Password')"
viewable
placeholder="{{ __('Password') }}"
/>
<flux:button variant="primary" type="submit" class="w-full" data-test="confirm-password-button">
{{ __('Confirm') }}
</flux:button>
<x-button type="submit" label="{{ __('Confirm') }}" class="btn-primary w-full" data-test="confirm-password-button" />
</form>
</div>
</x-layouts::auth>
@@ -9,23 +9,22 @@
@csrf
<!-- Email Address -->
<flux:input
<x-input
name="email"
:label="__('Email Address')"
label="{{ __('Email Address') }}"
type="email"
required
autofocus
placeholder="email@example.com"
icon="o-envelope"
/>
<flux:button variant="primary" type="submit" class="w-full" data-test="email-password-reset-link-button">
{{ __('Email password reset link') }}
</flux:button>
<x-button type="submit" label="{{ __('Email password reset link') }}" class="btn-primary w-full" data-test="email-password-reset-link-button" />
</form>
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-400">
<div class="space-x-1 rtl:space-x-reverse text-center text-sm opacity-60">
<span>{{ __('Or, return to') }}</span>
<flux:link :href="route('login')" wire:navigate>{{ __('log in') }}</flux:link>
<a href="{{ route('login') }}" class="link link-primary" wire:navigate>{{ __('log in') }}</a>
</div>
</div>
</x-layouts::auth>
+15 -15
View File
@@ -9,50 +9,50 @@
@csrf
<!-- Email Address -->
<flux:input
<x-input
name="email"
:label="__('Email address')"
label="{{ __('Email address') }}"
:value="old('email')"
type="email"
required
autofocus
autocomplete="email"
placeholder="email@example.com"
icon="o-envelope"
/>
<!-- Password -->
<div class="relative">
<flux:input
<x-password
name="password"
:label="__('Password')"
type="password"
label="{{ __('Password') }}"
required
autocomplete="current-password"
:placeholder="__('Password')"
viewable
placeholder="{{ __('Password') }}"
/>
@if (Route::has('password.request'))
<flux:link class="absolute top-0 text-sm end-0" :href="route('password.request')" wire:navigate>
<a class="absolute top-0 text-sm end-0 link link-primary" href="{{ route('password.request') }}" wire:navigate>
{{ __('Forgot your password?') }}
</flux:link>
</a>
@endif
</div>
<!-- Remember Me -->
<flux:checkbox name="remember" :label="__('Remember me')" :checked="old('remember')" />
<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">
<flux:button variant="primary" type="submit" class="w-full" data-test="login-button">
{{ __('Log in') }}
</flux:button>
<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 text-zinc-600 dark:text-zinc-400">
<div class="space-x-1 text-sm text-center rtl:space-x-reverse opacity-60">
<span>{{ __('Don\'t have an account?') }}</span>
<flux:link :href="route('register')" wire:navigate>{{ __('Sign up') }}</flux:link>
<a href="{{ route('register') }}" class="link link-primary" wire:navigate>{{ __('Sign up') }}</a>
</div>
@endif
</div>
+16 -20
View File
@@ -8,60 +8,56 @@
<form method="POST" action="{{ route('register.store') }}" class="flex flex-col gap-6">
@csrf
<!-- Name -->
<flux:input
<x-input
name="name"
:label="__('Name')"
label="{{ __('Name') }}"
:value="old('name')"
type="text"
required
autofocus
autocomplete="name"
:placeholder="__('Full name')"
placeholder="{{ __('Full name') }}"
icon="o-user"
/>
<!-- Email Address -->
<flux:input
<x-input
name="email"
:label="__('Email address')"
label="{{ __('Email address') }}"
:value="old('email')"
type="email"
required
autocomplete="email"
placeholder="email@example.com"
icon="o-envelope"
/>
<!-- Password -->
<flux:input
<x-password
name="password"
:label="__('Password')"
type="password"
label="{{ __('Password') }}"
required
autocomplete="new-password"
:placeholder="__('Password')"
viewable
placeholder="{{ __('Password') }}"
/>
<!-- Confirm Password -->
<flux:input
<x-password
name="password_confirmation"
:label="__('Confirm password')"
type="password"
label="{{ __('Confirm password') }}"
required
autocomplete="new-password"
:placeholder="__('Confirm password')"
viewable
placeholder="{{ __('Confirm password') }}"
/>
<div class="flex items-center justify-end">
<flux:button type="submit" variant="primary" class="w-full" data-test="register-user-button">
{{ __('Create account') }}
</flux:button>
<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 text-zinc-600 dark:text-zinc-400">
<div class="space-x-1 rtl:space-x-reverse text-center text-sm opacity-60">
<span>{{ __('Already have an account?') }}</span>
<flux:link :href="route('login')" wire:navigate>{{ __('Log in') }}</flux:link>
<a href="{{ route('login') }}" class="link link-primary" wire:navigate>{{ __('Log in') }}</a>
</div>
</div>
</x-layouts::auth>
@@ -11,41 +11,36 @@
<input type="hidden" name="token" value="{{ request()->route('token') }}">
<!-- Email Address -->
<flux:input
<x-input
name="email"
value="{{ request('email') }}"
:label="__('Email')"
label="{{ __('Email') }}"
type="email"
required
autocomplete="email"
icon="o-envelope"
/>
<!-- Password -->
<flux:input
<x-password
name="password"
:label="__('Password')"
type="password"
label="{{ __('Password') }}"
required
autocomplete="new-password"
:placeholder="__('Password')"
viewable
placeholder="{{ __('Password') }}"
/>
<!-- Confirm Password -->
<flux:input
<x-password
name="password_confirmation"
:label="__('Confirm password')"
type="password"
label="{{ __('Confirm password') }}"
required
autocomplete="new-password"
:placeholder="__('Confirm password')"
viewable
placeholder="{{ __('Confirm password') }}"
/>
<div class="flex items-center justify-end">
<flux:button type="submit" variant="primary" class="w-full" data-test="reset-password-button">
{{ __('Reset password') }}
</flux:button>
<x-button type="submit" label="{{ __('Reset password') }}" class="btn-primary w-full" data-test="reset-password-button" />
</div>
</form>
</div>
@@ -9,16 +9,12 @@
recovery_code: '',
toggleInput() {
this.showRecoveryInput = !this.showRecoveryInput;
this.code = '';
this.recovery_code = '';
$dispatch('clear-2fa-auth-code');
$nextTick(() => {
this.showRecoveryInput
? this.$refs.recovery_code?.focus()
: $dispatch('focus-2fa-auth-code');
: this.$refs.code?.focus();
});
},
}"
@@ -42,21 +38,24 @@
<div class="space-y-5 text-center">
<div x-show="!showRecoveryInput">
<div class="flex items-center justify-center my-5">
<flux:otp
x-model="code"
length="6"
<div class="my-5">
<x-input
type="text"
name="code"
label="OTP Code"
label:sr-only
class="mx-auto"
/>
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">
<flux:input
<x-input
type="text"
name="recovery_code"
x-ref="recovery_code"
@@ -67,19 +66,15 @@
</div>
@error('recovery_code')
<flux:text color="red">
{{ $message }}
</flux:text>
<p class="text-error text-sm">{{ $message }}</p>
@enderror
</div>
<flux:button
variant="primary"
<x-button
type="submit"
class="w-full"
>
{{ __('Continue') }}
</flux:button>
label="{{ __('Continue') }}"
class="btn-primary w-full"
/>
</div>
<div class="mt-5 space-x-0.5 text-sm leading-5 text-center">
@@ -1,28 +1,24 @@
<x-layouts::auth>
<div class="mt-4 flex flex-col gap-6">
<flux:text class="text-center">
<p class="text-center text-sm opacity-70">
{{ __('Please verify your email address by clicking on the link we just emailed to you.') }}
</flux:text>
</p>
@if (session('status') == 'verification-link-sent')
<flux:text class="text-center font-medium !dark:text-green-400 !text-green-600">
<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.') }}
</flux:text>
</p>
@endif
<div class="flex flex-col items-center justify-between space-y-3">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<flux:button type="submit" variant="primary" class="w-full">
{{ __('Resend verification email') }}
</flux:button>
<x-button type="submit" label="{{ __('Resend verification email') }}" class="btn-primary w-full" />
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<flux:button variant="ghost" type="submit" class="text-sm cursor-pointer" data-test="logout-button">
{{ __('Log out') }}
</flux:button>
<x-button type="submit" label="{{ __('Log out') }}" class="btn-ghost btn-sm" data-test="logout-button" />
</form>
</div>
</div>
@@ -1,20 +1,20 @@
<div class="flex items-start max-md:flex-col">
<div class="me-10 w-full pb-4 md:w-[220px]">
<flux:navlist aria-label="{{ __('Settings') }}">
<flux:navlist.item :href="route('profile.edit')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
<flux:navlist.item :href="route('user-password.edit')" wire:navigate>{{ __('Password') }}</flux:navlist.item>
<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())
<flux:navlist.item :href="route('two-factor.show')" wire:navigate>{{ __('Two-Factor Auth') }}</flux:navlist.item>
<x-menu-item title="{{ __('Two-Factor Auth') }}" link="{{ route('two-factor.show') }}" wire:navigate />
@endif
<flux:navlist.item :href="route('appearance.edit')" wire:navigate>{{ __('Appearance') }}</flux:navlist.item>
</flux:navlist>
<x-menu-item title="{{ __('Appearance') }}" link="{{ route('appearance.edit') }}" wire:navigate />
</x-menu>
</div>
<flux:separator class="md:hidden" />
<div class="divider md:hidden"></div>
<div class="flex-1 self-stretch max-md:pt-6">
<flux:heading>{{ $heading ?? '' }}</flux:heading>
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
<h2 class="text-lg font-semibold">{{ $heading ?? '' }}</h2>
<p class="text-sm opacity-60">{{ $subheading ?? '' }}</p>
<div class="mt-5 w-full max-w-lg">
{{ $slot }}
@@ -46,55 +46,46 @@ new class extends Component {
}; ?>
<div
class="py-6 space-y-6 border shadow-sm rounded-xl border-zinc-200 dark:border-white/10"
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">
<flux:icon.lock-closed variant="outline" class="size-4"/>
<flux:heading size="lg" level="3">{{ __('2FA Recovery Codes') }}</flux:heading>
<x-icon name="o-lock-closed" class="w-4 h-4" />
<h3 class="text-lg font-semibold">{{ __('2FA Recovery Codes') }}</h3>
</div>
<flux:text variant="subtle">
<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.') }}
</flux:text>
</p>
</div>
<div class="px-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<flux:button
<x-button
x-show="!showRecoveryCodes"
icon="eye"
icon:variant="outline"
variant="primary"
icon="o-eye"
label="{{ __('View Recovery Codes') }}"
class="btn-primary btn-sm"
@click="showRecoveryCodes = true;"
aria-expanded="false"
aria-controls="recovery-codes-section"
>
{{ __('View Recovery Codes') }}
</flux:button>
/>
<flux:button
<x-button
x-show="showRecoveryCodes"
icon="eye-slash"
icon:variant="outline"
variant="primary"
icon="o-eye-slash"
label="{{ __('Hide Recovery Codes') }}"
class="btn-primary btn-sm"
@click="showRecoveryCodes = false"
aria-expanded="true"
aria-controls="recovery-codes-section"
>
{{ __('Hide Recovery Codes') }}
</flux:button>
/>
@if (filled($recoveryCodes))
<flux:button
<x-button
x-show="showRecoveryCodes"
icon="arrow-path"
variant="filled"
icon="o-arrow-path"
label="{{ __('Regenerate Codes') }}"
class="btn-sm"
wire:click="regenerateRecoveryCodes"
>
{{ __('Regenerate Codes') }}
</flux:button>
/>
@endif
</div>
@@ -107,12 +98,12 @@ new class extends Component {
>
<div class="mt-3 space-y-3">
@error('recoveryCodes')
<flux:callout variant="danger" icon="x-circle" heading="{{$message}}"/>
<div class="alert alert-error">{{ $message }}</div>
@enderror
@if (filled($recoveryCodes))
<div
class="grid gap-1 p-4 font-mono text-sm rounded-lg bg-zinc-100 dark:bg-white/5"
class="grid gap-1 p-4 font-mono text-sm rounded-lg bg-base-200"
role="list"
aria-label="{{ __('Recovery codes') }}"
>
@@ -126,9 +117,9 @@ new class extends Component {
</div>
@endforeach
</div>
<flux:text variant="subtle" class="text-xs">
<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.') }}
</flux:text>
</p>
@endif
</div>
</div>
@@ -9,13 +9,7 @@ new class extends Component {
<section class="w-full">
@include('partials.settings-heading')
<flux:heading class="sr-only">{{ __('Appearance Settings') }}</flux:heading>
<x-pages::settings.layout :heading="__('Appearance')" :subheading="__('Update the appearance settings for your account')">
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance">
<flux:radio value="light" icon="sun">{{ __('Light') }}</flux:radio>
<flux:radio value="dark" icon="moon">{{ __('Dark') }}</flux:radio>
<flux:radio value="system" icon="computer-desktop">{{ __('System') }}</flux:radio>
</flux:radio.group>
<x-theme-toggle />
</x-pages::settings.layout>
</section>
@@ -9,6 +9,7 @@ new class extends Component {
use PasswordValidationRules;
public string $password = '';
public bool $showDeleteModal = false;
/**
* Delete the currently authenticated user.
@@ -27,37 +28,29 @@ new class extends Component {
<section class="mt-10 space-y-6">
<div class="relative mb-5">
<flux:heading>{{ __('Delete account') }}</flux:heading>
<flux:subheading>{{ __('Delete your account and all of its resources') }}</flux:subheading>
<h3 class="text-lg font-semibold">{{ __('Delete account') }}</h3>
<p class="text-sm opacity-60">{{ __('Delete your account and all of its resources') }}</p>
</div>
<flux:modal.trigger name="confirm-user-deletion">
<flux:button variant="danger" x-data="" x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')" data-test="delete-user-button">
{{ __('Delete account') }}
</flux:button>
</flux:modal.trigger>
<x-button
label="{{ __('Delete account') }}"
class="btn-error"
@click="$wire.showDeleteModal = true"
data-test="delete-user-button"
/>
<flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
<form method="POST" wire:submit="deleteUser" class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Are you sure you want to delete your account?') }}</flux:heading>
<x-modal wire:model="showDeleteModal" title="{{ __('Are you sure you want to delete your account?') }}">
<p class="text-sm opacity-70">
{{ __('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>
<flux:subheading>
{{ __('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.') }}
</flux:subheading>
</div>
<form wire:submit="deleteUser" class="space-y-6 mt-4">
<x-password wire:model="password" label="{{ __('Password') }}" />
<flux:input wire:model="password" :label="__('Password')" type="password" />
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Cancel') }}</flux:button>
</flux:modal.close>
<flux:button variant="danger" type="submit" data-test="confirm-delete-user-button">
{{ __('Delete account') }}
</flux:button>
</div>
<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>
</flux:modal>
</x-modal>
</section>
@@ -42,37 +42,30 @@ new class extends Component {
<section class="w-full">
@include('partials.settings-heading')
<flux:heading class="sr-only">{{ __('Password Settings') }}</flux: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">
<flux:input
<x-password
wire:model="current_password"
:label="__('Current password')"
type="password"
label="{{ __('Current password') }}"
required
autocomplete="current-password"
/>
<flux:input
<x-password
wire:model="password"
:label="__('New password')"
type="password"
label="{{ __('New password') }}"
required
autocomplete="new-password"
/>
<flux:input
<x-password
wire:model="password_confirmation"
:label="__('Confirm Password')"
type="password"
label="{{ __('Confirm Password') }}"
required
autocomplete="new-password"
/>
<div class="flex items-center gap-4">
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full" data-test="update-password-button">
{{ __('Save') }}
</flux:button>
<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">
@@ -79,29 +79,27 @@ new class extends Component {
<section class="w-full">
@include('partials.settings-heading')
<flux:heading class="sr-only">{{ __('Profile Settings') }}</flux: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">
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
<x-input wire:model="name" label="{{ __('Name') }}" type="text" required autofocus autocomplete="name" icon="o-user" />
<div>
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
<x-input wire:model="email" label="{{ __('Email') }}" type="email" required autocomplete="email" icon="o-envelope" />
@if ($this->hasUnverifiedEmail)
<div>
<flux:text class="mt-4">
<p class="mt-4 text-sm opacity-70">
{{ __('Your email address is unverified.') }}
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
<a class="link link-primary text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
{{ __('Click here to re-send the verification email.') }}
</flux:link>
</flux:text>
</a>
</p>
@if (session('status') === 'verification-link-sent')
<flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
<p class="mt-2 text-sm font-medium text-success">
{{ __('A new verification link has been sent to your email address.') }}
</flux:text>
</p>
@endif
</div>
@endif
@@ -109,9 +107,7 @@ new class extends Component {
<div class="flex items-center gap-4">
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full" data-test="update-profile-button">
{{ __('Save') }}
</flux:button>
<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">
@@ -180,8 +180,6 @@ new class extends Component {
<section class="w-full">
@include('partials.settings-heading')
<flux:heading class="sr-only">{{ __('Two-Factor Authentication Settings') }}</flux:heading>
<x-pages::settings.layout
:heading="__('Two Factor Authentication')"
:subheading="__('Manage your two-factor authentication settings')"
@@ -190,200 +188,141 @@ new class extends Component {
@if ($twoFactorEnabled)
<div class="space-y-4">
<div class="flex items-center gap-3">
<flux:badge color="green">{{ __('Enabled') }}</flux:badge>
<span class="badge badge-success">{{ __('Enabled') }}</span>
</div>
<flux:text>
<p class="opacity-70">
{{ __('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.') }}
</flux:text>
</p>
<livewire:pages::settings.two-factor.recovery-codes :$requiresConfirmation />
<div class="flex justify-start">
<flux:button
variant="danger"
icon="shield-exclamation"
icon:variant="outline"
<x-button
label="{{ __('Disable 2FA') }}"
icon="o-shield-exclamation"
class="btn-error"
wire:click="disable"
>
{{ __('Disable 2FA') }}
</flux:button>
/>
</div>
</div>
@else
<div class="space-y-4">
<div class="flex items-center gap-3">
<flux:badge color="red">{{ __('Disabled') }}</flux:badge>
<span class="badge badge-error">{{ __('Disabled') }}</span>
</div>
<flux:text variant="subtle">
<p class="opacity-60">
{{ __('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.') }}
</flux:text>
</p>
<flux:button
variant="primary"
icon="shield-check"
icon:variant="outline"
<x-button
label="{{ __('Enable 2FA') }}"
icon="o-shield-check"
class="btn-primary"
wire:click="enable"
>
{{ __('Enable 2FA') }}
</flux:button>
/>
</div>
@endif
</div>
</x-pages::settings.layout>
<flux:modal
name="two-factor-setup-modal"
class="max-w-md md:min-w-md"
@close="closeModal"
wire:model="showModal"
>
<div class="space-y-6">
<div class="flex flex-col items-center space-y-4">
<div class="p-0.5 w-auto rounded-full border border-stone-100 dark:border-stone-600 bg-white dark:bg-stone-800 shadow-sm">
<div class="p-2.5 rounded-full border border-stone-200 dark:border-stone-600 overflow-hidden bg-stone-100 dark:bg-stone-200 relative">
<div class="flex items-stretch absolute inset-0 w-full h-full divide-x [&>div]:flex-1 divide-stone-200 dark:divide-stone-300 justify-around opacity-50">
@for ($i = 1; $i <= 5; $i++)
<div></div>
@endfor
</div>
<x-modal wire:model="showModal" :title="$this->modalConfig['title']" class="max-w-md">
<p class="text-sm opacity-70">{{ $this->modalConfig['description'] }}</p>
<div class="flex flex-col items-stretch absolute w-full h-full divide-y [&>div]:flex-1 inset-0 divide-stone-200 dark:divide-stone-300 justify-around opacity-50">
@for ($i = 1; $i <= 5; $i++)
<div></div>
@endfor
</div>
<flux:icon.qr-code class="relative z-20 dark:text-accent-foreground"/>
</div>
@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>
<div class="space-y-2 text-center">
<flux:heading size="lg">{{ $this->modalConfig['title'] }}</flux:heading>
<flux:text>{{ $this->modalConfig['description'] }}</flux:text>
<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>
@else
@error('setupData')
<div class="alert alert-error mt-4">{{ $message }}</div>
@enderror
<div class="flex justify-center mt-4">
<div class="relative w-64 overflow-hidden border rounded-lg border-base-300 aspect-square">
@empty($qrCodeSvg)
<div class="absolute inset-0 flex items-center justify-center bg-base-200 animate-pulse">
<span class="loading loading-spinner"></span>
</div>
@else
<div class="flex items-center justify-center h-full p-4 bg-white">
{!! $qrCodeSvg !!}
</div>
@endempty
</div>
</div>
@if ($showVerificationStep)
<div class="space-y-6">
<div class="flex flex-col items-center space-y-3 justify-center">
<flux:otp
name="code"
wire:model="code"
length="6"
label="OTP Code"
label:sr-only
class="mx-auto"
/>
</div>
<div class="flex items-center space-x-3">
<flux:button
variant="outline"
class="flex-1"
wire:click="resetVerification"
>
{{ __('Back') }}
</flux:button>
<flux:button
variant="primary"
class="flex-1"
wire:click="confirmTwoFactor"
x-bind:disabled="$wire.code.length < 6"
>
{{ __('Confirm') }}
</flux:button>
</div>
</div>
@else
@error('setupData')
<flux:callout variant="danger" icon="x-circle" heading="{{ $message }}"/>
@enderror
<div class="flex justify-center">
<div class="relative w-64 overflow-hidden border rounded-lg border-stone-200 dark:border-stone-700 aspect-square">
@empty($qrCodeSvg)
<div class="absolute inset-0 flex items-center justify-center bg-white dark:bg-stone-700 animate-pulse">
<flux:icon.loading/>
</div>
@else
<div x-data class="flex items-center justify-center h-full p-4">
<div
class="bg-white p-3 rounded"
:style="($flux.appearance === 'dark' || ($flux.appearance === 'system' && $flux.dark)) ? 'filter: invert(1) brightness(1.5)' : ''"
>
{!! $qrCodeSvg !!}
</div>
</div>
@endempty
</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>
<flux:button
:disabled="$errors->has('setupData')"
variant="primary"
class="w-full"
wire:click="showVerificationIfNecessary"
>
{{ $this->modalConfig['buttonText'] }}
</flux:button>
</div>
<div class="space-y-4">
<div class="relative flex items-center justify-center w-full">
<div class="absolute inset-0 w-full h-px top-1/2 bg-stone-200 dark:bg-stone-600"></div>
<span class="relative px-2 text-sm bg-white dark:bg-stone-800 text-stone-600 dark:text-stone-400">
{{ __('or, enter the code manually') }}
</span>
</div>
<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="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="flex items-stretch w-full border rounded-xl dark:border-stone-700">
@empty($manualSetupKey)
<div class="flex items-center justify-center w-full p-3 bg-stone-100 dark:bg-stone-700">
<flux:icon.loading variant="mini"/>
</div>
@else
<input
type="text"
readonly
value="{{ $manualSetupKey }}"
class="w-full p-3 bg-transparent outline-none text-stone-900 dark:text-stone-100"
/>
<button
@click="copy()"
class="px-3 transition-colors border-l cursor-pointer border-stone-200 dark:border-stone-600"
>
<flux:icon.document-duplicate x-show="!copied" variant="outline"></flux:icon>
<flux:icon.check
x-show="copied"
variant="solid"
class="text-green-500"
></flux:icon>
</button>
@endempty
</div>
}
}"
>
<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>
@endif
</div>
</flux:modal>
</div>
<x-slot:actions>
<x-button
:disabled="$errors->has('setupData')"
label="{{ $this->modalConfig['buttonText'] }}"
class="btn-primary"
wire:click="showVerificationIfNecessary"
/>
</x-slot:actions>
@endif
</x-modal>
</section>
+1 -2
View File
@@ -1,7 +1,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ $title ?? config('app.name') }}</title>
<title>{{ $title ?? (\App\Models\Setting::get('site_title') ?: config('app.name')) }}</title>
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
@@ -11,4 +11,3 @@
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
@fluxAppearance
@@ -1,5 +1,5 @@
<div class="relative mb-6 w-full">
<flux:heading size="xl" level="1">{{ __('Settings') }}</flux:heading>
<flux:subheading size="lg" class="mb-6">{{ __('Manage your profile and account settings') }}</flux:subheading>
<flux:separator variant="subtle" />
<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>
File diff suppressed because one or more lines are too long