formatted, added team index, create, show
This commit is contained in:
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\TeamMemberRole;
|
||||||
use App\Http\Requests\StoreTeamRequest;
|
use App\Http\Requests\StoreTeamRequest;
|
||||||
use App\Http\Requests\UpdateTeamRequest;
|
use App\Http\Requests\UpdateTeamRequest;
|
||||||
use App\Http\Resources\TeamResource;
|
use App\Http\Resources\TeamResource;
|
||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
class TeamController extends Controller
|
class TeamController extends Controller
|
||||||
@@ -17,13 +19,17 @@ class TeamController extends Controller
|
|||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
return Inertia::render('team/Index', [
|
return Inertia::render('team/Index', [
|
||||||
'teams' => TeamResource::collection(Team::visibleTo($request->user())->get())->resolve($request),
|
'teams' => TeamResource::collection(Team::visibleTo(auth()->user())->get())->resolve($request),
|
||||||
|
'canCreateTeam' => auth()->user()->can('create', Team::class),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function show(Request$request, Team $team)
|
||||||
{
|
{
|
||||||
//
|
return Inertia::render('team/Show', [
|
||||||
|
'team' => TeamResource::make($team)->resolve($request),
|
||||||
|
'canManageTeam' => auth()->user()->can('manage', $team),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,9 +37,29 @@ class TeamController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(StoreTeamRequest $request)
|
public function store(StoreTeamRequest $request)
|
||||||
{
|
{
|
||||||
//
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($validated) {
|
||||||
|
$team = Team::create($validated);
|
||||||
|
$team->members()->attach(
|
||||||
|
request()->user()->id,
|
||||||
|
['role' => TeamMemberRole::Admin]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Inertia::flash('success', 'Team successfully created.');
|
||||||
|
return to_route('teams.index');
|
||||||
|
} catch (\Exception $exception) {
|
||||||
|
Inertia::flash('error', 'Team could not be created.');
|
||||||
|
DB::rollBack();
|
||||||
|
return back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return Inertia::render('team/Create');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for editing the specified resource.
|
* Show the form for editing the specified resource.
|
||||||
|
|||||||
@@ -28,4 +28,14 @@ class StoreTeamRequest extends FormRequest
|
|||||||
'slug' => Str::slug($this->name),
|
'slug' => Str::slug($this->name),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'slug.string' => '',
|
||||||
|
'slug.required' => '',
|
||||||
|
'slug.max' => '',
|
||||||
|
'slug.unique' => 'The name has already been taken.',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class TeamMemberResource extends JsonResource
|
|||||||
'email' => $this->email,
|
'email' => $this->email,
|
||||||
'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(),
|
'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(),
|
||||||
'role' => $this->pivot->role,
|
'role' => $this->pivot->role,
|
||||||
|
'joined_at' => $this->pivot->created_at->toDateTimeString(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ class TeamResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
'members' => TeamMemberResource::collection($this->members),
|
'members' => TeamMemberResource::collection($this->members)->resolve($request),
|
||||||
'multi_factor_required' => $this->require_2fa,
|
'multi_factor_required' => $this->require_2fa,
|
||||||
|
'created_at' => $this->created_at->toDateTimeString(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://shadcn-vue.com/schema.json",
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
"style": "new-york-v4",
|
"style": "new-york",
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.js",
|
||||||
"css": "resources/css/app.css",
|
"css": "resources/css/app.css",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-ide-helper": "^3.7",
|
"barryvdh/laravel-ide-helper": "^3.7",
|
||||||
"fakerphp/faker": "^1.24",
|
"fakerphp/faker": "^1.24",
|
||||||
"larastan/larastan": "^3.9",
|
|
||||||
"laravel/boost": "^2.2",
|
"laravel/boost": "^2.2",
|
||||||
"laravel/pail": "^1.2.5",
|
"laravel/pail": "^1.2.5",
|
||||||
"laravel/pao": "^1.0.6",
|
"laravel/pao": "^1.0.6",
|
||||||
@@ -69,13 +68,9 @@
|
|||||||
"pnpm run types:check",
|
"pnpm run types:check",
|
||||||
"@test"
|
"@test"
|
||||||
],
|
],
|
||||||
"types:check": [
|
|
||||||
"phpstan analyse --memory-limit=512M"
|
|
||||||
],
|
|
||||||
"test": [
|
"test": [
|
||||||
"@php artisan config:clear --ansi",
|
"@php artisan config:clear --ansi",
|
||||||
"@lint:check",
|
"@lint:check",
|
||||||
"@types:check",
|
|
||||||
"@php artisan test"
|
"@php artisan test"
|
||||||
],
|
],
|
||||||
"post-autoload-dump": [
|
"post-autoload-dump": [
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"build:ssr": "vp build && vp build --ssr",
|
"build:ssr": "vp build && vp build --ssr",
|
||||||
"dev": "vp dev",
|
"dev": "vp dev",
|
||||||
"check": "vp check",
|
"check": "vp check",
|
||||||
"check:fix": "vp check --fix",
|
"format": "vp check --fix",
|
||||||
"types:check": "vue-tsc --noEmit"
|
"types:check": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
includes:
|
|
||||||
- vendor/larastan/larastan/extension.neon
|
|
||||||
- vendor/nesbot/carbon/extension.neon
|
|
||||||
|
|
||||||
parameters:
|
|
||||||
paths:
|
|
||||||
- app/
|
|
||||||
- bootstrap/app.php
|
|
||||||
- config/
|
|
||||||
- database/
|
|
||||||
- routes/
|
|
||||||
|
|
||||||
level: 7
|
|
||||||
+125
-126
@@ -8,57 +8,56 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans:
|
--font-sans:
|
||||||
Instrument Sans, ui-sans-serif, system-ui, sans-serif,
|
Instrument Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
||||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
'Noto Color Emoji';
|
|
||||||
|
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
|
||||||
--color-secondary: var(--secondary);
|
--color-secondary: var(--secondary);
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-ring: var(--ring);
|
--color-ring: var(--ring);
|
||||||
|
|
||||||
--color-chart-1: var(--chart-1);
|
--color-chart-1: var(--chart-1);
|
||||||
--color-chart-2: var(--chart-2);
|
--color-chart-2: var(--chart-2);
|
||||||
--color-chart-3: var(--chart-3);
|
--color-chart-3: var(--chart-3);
|
||||||
--color-chart-4: var(--chart-4);
|
--color-chart-4: var(--chart-4);
|
||||||
--color-chart-5: var(--chart-5);
|
--color-chart-5: var(--chart-5);
|
||||||
|
|
||||||
--color-sidebar: var(--sidebar-background);
|
--color-sidebar: var(--sidebar-background);
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -70,103 +69,103 @@
|
|||||||
color utility to any element that depends on these defaults.
|
color utility to any element that depends on these defaults.
|
||||||
*/
|
*/
|
||||||
@layer base {
|
@layer base {
|
||||||
*,
|
*,
|
||||||
::after,
|
::after,
|
||||||
::before,
|
::before,
|
||||||
::backdrop,
|
::backdrop,
|
||||||
::file-selector-button {
|
::file-selector-button {
|
||||||
border-color: var(--color-gray-200, currentColor);
|
border-color: var(--color-gray-200, currentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
--font-sans:
|
--font-sans:
|
||||||
'Instrument Sans', ui-sans-serif, system-ui, sans-serif,
|
'Instrument Sans', ui-sans-serif, system-ui, sans-serif,
|
||||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
'Noto Color Emoji';
|
'Noto Color Emoji';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: hsl(0 0% 100%);
|
--background: hsl(0 0% 100%);
|
||||||
--foreground: hsl(0 0% 3.9%);
|
--foreground: hsl(0 0% 3.9%);
|
||||||
--card: hsl(0 0% 100%);
|
--card: hsl(0 0% 100%);
|
||||||
--card-foreground: hsl(0 0% 3.9%);
|
--card-foreground: hsl(0 0% 3.9%);
|
||||||
--popover: hsl(0 0% 100%);
|
--popover: hsl(0 0% 100%);
|
||||||
--popover-foreground: hsl(0 0% 3.9%);
|
--popover-foreground: hsl(0 0% 3.9%);
|
||||||
--primary: hsl(0 0% 9%);
|
--primary: hsl(0 0% 9%);
|
||||||
--primary-foreground: hsl(0 0% 98%);
|
--primary-foreground: hsl(0 0% 98%);
|
||||||
--secondary: hsl(0 0% 92.1%);
|
--secondary: hsl(0 0% 92.1%);
|
||||||
--secondary-foreground: hsl(0 0% 9%);
|
--secondary-foreground: hsl(0 0% 9%);
|
||||||
--muted: hsl(0 0% 96.1%);
|
--muted: hsl(0 0% 96.1%);
|
||||||
--muted-foreground: hsl(0 0% 45.1%);
|
--muted-foreground: hsl(0 0% 45.1%);
|
||||||
--accent: hsl(0 0% 96.1%);
|
--accent: hsl(0 0% 96.1%);
|
||||||
--accent-foreground: hsl(0 0% 9%);
|
--accent-foreground: hsl(0 0% 9%);
|
||||||
--destructive: hsl(0 84.2% 60.2%);
|
--destructive: hsl(0 84.2% 60.2%);
|
||||||
--destructive-foreground: hsl(0 0% 98%);
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
--border: hsl(0 0% 92.8%);
|
--border: hsl(0 0% 92.8%);
|
||||||
--input: hsl(0 0% 89.8%);
|
--input: hsl(0 0% 89.8%);
|
||||||
--ring: hsl(0 0% 3.9%);
|
--ring: hsl(0 0% 3.9%);
|
||||||
--chart-1: hsl(12 76% 61%);
|
--chart-1: hsl(12 76% 61%);
|
||||||
--chart-2: hsl(173 58% 39%);
|
--chart-2: hsl(173 58% 39%);
|
||||||
--chart-3: hsl(197 37% 24%);
|
--chart-3: hsl(197 37% 24%);
|
||||||
--chart-4: hsl(43 74% 66%);
|
--chart-4: hsl(43 74% 66%);
|
||||||
--chart-5: hsl(27 87% 67%);
|
--chart-5: hsl(27 87% 67%);
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--sidebar-background: hsl(0 0% 98%);
|
--sidebar-background: hsl(0 0% 98%);
|
||||||
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
||||||
--sidebar-primary: hsl(0 0% 10%);
|
--sidebar-primary: hsl(0 0% 10%);
|
||||||
--sidebar-primary-foreground: hsl(0 0% 98%);
|
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||||
--sidebar-accent: hsl(0 0% 94%);
|
--sidebar-accent: hsl(0 0% 94%);
|
||||||
--sidebar-accent-foreground: hsl(0 0% 30%);
|
--sidebar-accent-foreground: hsl(0 0% 30%);
|
||||||
--sidebar-border: hsl(0 0% 91%);
|
--sidebar-border: hsl(0 0% 91%);
|
||||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
--sidebar: hsl(0 0% 98%);
|
--sidebar: hsl(0 0% 98%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: hsl(0 0% 3.9%);
|
--background: hsl(0 0% 3.9%);
|
||||||
--foreground: hsl(0 0% 98%);
|
--foreground: hsl(0 0% 98%);
|
||||||
--card: hsl(0 0% 3.9%);
|
--card: hsl(0 0% 3.9%);
|
||||||
--card-foreground: hsl(0 0% 98%);
|
--card-foreground: hsl(0 0% 98%);
|
||||||
--popover: hsl(0 0% 3.9%);
|
--popover: hsl(0 0% 3.9%);
|
||||||
--popover-foreground: hsl(0 0% 98%);
|
--popover-foreground: hsl(0 0% 98%);
|
||||||
--primary: hsl(0 0% 98%);
|
--primary: hsl(0 0% 98%);
|
||||||
--primary-foreground: hsl(0 0% 9%);
|
--primary-foreground: hsl(0 0% 9%);
|
||||||
--secondary: hsl(0 0% 14.9%);
|
--secondary: hsl(0 0% 14.9%);
|
||||||
--secondary-foreground: hsl(0 0% 98%);
|
--secondary-foreground: hsl(0 0% 98%);
|
||||||
--muted: hsl(0 0% 16.08%);
|
--muted: hsl(0 0% 16.08%);
|
||||||
--muted-foreground: hsl(0 0% 63.9%);
|
--muted-foreground: hsl(0 0% 63.9%);
|
||||||
--accent: hsl(0 0% 14.9%);
|
--accent: hsl(0 0% 14.9%);
|
||||||
--accent-foreground: hsl(0 0% 98%);
|
--accent-foreground: hsl(0 0% 98%);
|
||||||
--destructive: hsl(0 84% 60%);
|
--destructive: hsl(0 84% 60%);
|
||||||
--destructive-foreground: hsl(0 0% 98%);
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
--border: hsl(0 0% 14.9%);
|
--border: hsl(0 0% 14.9%);
|
||||||
--input: hsl(0 0% 14.9%);
|
--input: hsl(0 0% 14.9%);
|
||||||
--ring: hsl(0 0% 83.1%);
|
--ring: hsl(0 0% 83.1%);
|
||||||
--chart-1: hsl(220 70% 50%);
|
--chart-1: hsl(220 70% 50%);
|
||||||
--chart-2: hsl(160 60% 45%);
|
--chart-2: hsl(160 60% 45%);
|
||||||
--chart-3: hsl(30 80% 55%);
|
--chart-3: hsl(30 80% 55%);
|
||||||
--chart-4: hsl(280 65% 60%);
|
--chart-4: hsl(280 65% 60%);
|
||||||
--chart-5: hsl(340 75% 55%);
|
--chart-5: hsl(340 75% 55%);
|
||||||
--sidebar-background: hsl(0 0% 7%);
|
--sidebar-background: hsl(0 0% 7%);
|
||||||
--sidebar-foreground: hsl(0 0% 95.9%);
|
--sidebar-foreground: hsl(0 0% 95.9%);
|
||||||
--sidebar-primary: hsl(360, 100%, 100%);
|
--sidebar-primary: hsl(360, 100%, 100%);
|
||||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||||
--sidebar-accent: hsl(0 0% 15.9%);
|
--sidebar-accent: hsl(0 0% 15.9%);
|
||||||
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||||
--sidebar-border: hsl(0 0% 15.9%);
|
--sidebar-border: hsl(0 0% 15.9%);
|
||||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
--sidebar: hsl(240 5.9% 10%);
|
--sidebar: hsl(240 5.9% 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -8,22 +8,22 @@ import { initializeFlashToast } from '@/lib/flashToast';
|
|||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
|
|
||||||
void createInertiaApp({
|
void createInertiaApp({
|
||||||
title: (title) => (title ? `${title} - ${appName}` : appName),
|
title: (title) => (title ? `${title} - ${appName}` : appName),
|
||||||
layout: (name) => {
|
layout: (name) => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case name === 'Welcome':
|
case name === 'Welcome':
|
||||||
return null;
|
return null;
|
||||||
case name.startsWith('auth/'):
|
case name.startsWith('auth/'):
|
||||||
return AuthLayout;
|
return AuthLayout;
|
||||||
case name.startsWith('settings/'):
|
case name.startsWith('settings/'):
|
||||||
return [AppLayout, SettingsLayout];
|
return [AppLayout, SettingsLayout];
|
||||||
default:
|
default:
|
||||||
return AppLayout;
|
return AppLayout;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
progress: {
|
progress: {
|
||||||
color: '#4B5563',
|
color: '#4B5563',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// This will set light / dark mode on page load...
|
// This will set light / dark mode on page load...
|
||||||
|
|||||||
@@ -4,27 +4,27 @@ import { computed } from 'vue';
|
|||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
errors: string[];
|
errors: string[];
|
||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
title: 'Something went wrong.',
|
title: 'Something went wrong.',
|
||||||
});
|
});
|
||||||
|
|
||||||
const uniqueErrors = computed(() => Array.from(new Set(props.errors)));
|
const uniqueErrors = computed(() => Array.from(new Set(props.errors)));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertCircle class="size-4" />
|
<AlertCircle class="size-4" />
|
||||||
<AlertTitle>{{ title }}</AlertTitle>
|
<AlertTitle>{{ title }}</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<ul class="list-inside list-disc text-sm">
|
<ul class="list-inside list-disc text-sm">
|
||||||
<li v-for="(error, index) in uniqueErrors" :key="index">
|
<li v-for="(error, index) in uniqueErrors" :key="index">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ import { SidebarInset } from '@/components/ui/sidebar';
|
|||||||
import type { AppVariant } from '@/types';
|
import type { AppVariant } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
variant?: AppVariant;
|
variant?: AppVariant;
|
||||||
class?: string;
|
class?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
variant: 'sidebar',
|
variant: 'sidebar',
|
||||||
});
|
});
|
||||||
const className = computed(() => props.class);
|
const className = computed(() => props.class);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarInset v-if="props.variant === 'sidebar'" :class="className">
|
<SidebarInset v-if="props.variant === 'sidebar'" :class="className">
|
||||||
<slot />
|
<slot />
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
<main
|
<main
|
||||||
v-else
|
v-else
|
||||||
class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl"
|
class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl"
|
||||||
:class="className"
|
:class="className"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,28 +8,28 @@ import Breadcrumbs from '@/components/Breadcrumbs.vue';
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
navigationMenuTriggerStyle,
|
navigationMenuTriggerStyle,
|
||||||
} from '@/components/ui/navigation-menu';
|
} from '@/components/ui/navigation-menu';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
import UserMenuContent from '@/components/UserMenuContent.vue';
|
import UserMenuContent from '@/components/UserMenuContent.vue';
|
||||||
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
||||||
@@ -39,11 +39,11 @@ import { dashboard } from '@/routes';
|
|||||||
import type { BreadcrumbItem, NavItem } from '@/types';
|
import type { BreadcrumbItem, NavItem } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
breadcrumbs?: BreadcrumbItem[];
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
breadcrumbs: () => [],
|
breadcrumbs: () => [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = usePage();
|
const page = usePage();
|
||||||
@@ -51,233 +51,208 @@ const auth = computed(() => page.props.auth);
|
|||||||
const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl();
|
const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl();
|
||||||
|
|
||||||
const activeItemStyles =
|
const activeItemStyles =
|
||||||
'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100';
|
'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100';
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
const mainNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
href: dashboard(),
|
href: dashboard(),
|
||||||
icon: LayoutGrid,
|
icon: LayoutGrid,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rightNavItems: NavItem[] = [
|
const rightNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Repository',
|
title: 'Repository',
|
||||||
href: 'https://github.com/laravel/vue-starter-kit',
|
href: 'https://github.com/laravel/vue-starter-kit',
|
||||||
icon: Folder,
|
icon: Folder,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Documentation',
|
title: 'Documentation',
|
||||||
href: 'https://laravel.com/docs/starter-kits#vue',
|
href: 'https://laravel.com/docs/starter-kits#vue',
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="border-sidebar-border/80 border-b">
|
<div class="border-sidebar-border/80 border-b">
|
||||||
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
|
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
|
||||||
<!-- Mobile Menu -->
|
<!-- Mobile Menu -->
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden">
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger :as-child="true">
|
<SheetTrigger :as-child="true">
|
||||||
<Button
|
<Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
|
||||||
variant="ghost"
|
<Menu class="h-5 w-5" />
|
||||||
size="icon"
|
</Button>
|
||||||
class="mr-2 h-9 w-9"
|
</SheetTrigger>
|
||||||
>
|
<SheetContent side="left" class="w-[300px] p-6">
|
||||||
<Menu class="h-5 w-5" />
|
<SheetTitle class="sr-only">Navigation menu</SheetTitle>
|
||||||
</Button>
|
<SheetHeader class="flex justify-start text-left">
|
||||||
</SheetTrigger>
|
<AppLogoIcon
|
||||||
<SheetContent side="left" class="w-[300px] p-6">
|
class="size-6 fill-current text-black dark:text-white"
|
||||||
<SheetTitle class="sr-only"
|
/>
|
||||||
>Navigation menu</SheetTitle
|
</SheetHeader>
|
||||||
>
|
<div
|
||||||
<SheetHeader class="flex justify-start text-left">
|
class="flex h-full flex-1 flex-col justify-between space-y-4 py-6"
|
||||||
<AppLogoIcon
|
>
|
||||||
class="size-6 fill-current text-black dark:text-white"
|
<nav class="-mx-3 space-y-1">
|
||||||
/>
|
<Link
|
||||||
</SheetHeader>
|
v-for="item in mainNavItems"
|
||||||
<div
|
:key="item.title"
|
||||||
class="flex h-full flex-1 flex-col justify-between space-y-4 py-6"
|
:href="item.href"
|
||||||
>
|
class="hover:bg-accent flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium"
|
||||||
<nav class="-mx-3 space-y-1">
|
:class="whenCurrentUrl(item.href, activeItemStyles)"
|
||||||
<Link
|
>
|
||||||
v-for="item in mainNavItems"
|
<component
|
||||||
:key="item.title"
|
v-if="item.icon"
|
||||||
:href="item.href"
|
:is="item.icon"
|
||||||
class="hover:bg-accent flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium"
|
class="h-5 w-5"
|
||||||
:class="
|
/>
|
||||||
whenCurrentUrl(
|
{{ item.title }}
|
||||||
item.href,
|
</Link>
|
||||||
activeItemStyles,
|
</nav>
|
||||||
)
|
<div class="flex flex-col space-y-4">
|
||||||
"
|
<a
|
||||||
>
|
v-for="item in rightNavItems"
|
||||||
<component
|
:key="item.title"
|
||||||
v-if="item.icon"
|
:href="toUrl(item.href)"
|
||||||
:is="item.icon"
|
target="_blank"
|
||||||
class="h-5 w-5"
|
rel="noopener noreferrer"
|
||||||
/>
|
class="flex items-center space-x-2 text-sm font-medium"
|
||||||
{{ item.title }}
|
>
|
||||||
</Link>
|
<component
|
||||||
</nav>
|
v-if="item.icon"
|
||||||
<div class="flex flex-col space-y-4">
|
:is="item.icon"
|
||||||
<a
|
class="h-5 w-5"
|
||||||
v-for="item in rightNavItems"
|
/>
|
||||||
:key="item.title"
|
<span>{{ item.title }}</span>
|
||||||
:href="toUrl(item.href)"
|
</a>
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="flex items-center space-x-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
v-if="item.icon"
|
|
||||||
:is="item.icon"
|
|
||||||
class="h-5 w-5"
|
|
||||||
/>
|
|
||||||
<span>{{ item.title }}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link :href="dashboard()" class="flex items-center gap-x-2">
|
<Link :href="dashboard()" class="flex items-center gap-x-2">
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<!-- Desktop Menu -->
|
||||||
|
<div class="hidden h-full lg:flex lg:flex-1">
|
||||||
|
<NavigationMenu class="ml-10 flex h-full items-stretch">
|
||||||
|
<NavigationMenuList class="flex h-full items-stretch space-x-2">
|
||||||
|
<NavigationMenuItem
|
||||||
|
v-for="(item, index) in mainNavItems"
|
||||||
|
:key="index"
|
||||||
|
class="relative flex h-full items-center"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
:class="[
|
||||||
|
navigationMenuTriggerStyle(),
|
||||||
|
whenCurrentUrl(item.href, activeItemStyles),
|
||||||
|
'h-9 cursor-pointer px-3',
|
||||||
|
]"
|
||||||
|
:href="item.href"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
v-if="item.icon"
|
||||||
|
:is="item.icon"
|
||||||
|
class="mr-2 h-4 w-4"
|
||||||
|
/>
|
||||||
|
{{ item.title }}
|
||||||
</Link>
|
</Link>
|
||||||
|
<div
|
||||||
<!-- Desktop Menu -->
|
v-if="isCurrentUrl(item.href)"
|
||||||
<div class="hidden h-full lg:flex lg:flex-1">
|
class="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"
|
||||||
<NavigationMenu class="ml-10 flex h-full items-stretch">
|
></div>
|
||||||
<NavigationMenuList
|
</NavigationMenuItem>
|
||||||
class="flex h-full items-stretch space-x-2"
|
</NavigationMenuList>
|
||||||
>
|
</NavigationMenu>
|
||||||
<NavigationMenuItem
|
|
||||||
v-for="(item, index) in mainNavItems"
|
|
||||||
:key="index"
|
|
||||||
class="relative flex h-full items-center"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
:class="[
|
|
||||||
navigationMenuTriggerStyle(),
|
|
||||||
whenCurrentUrl(
|
|
||||||
item.href,
|
|
||||||
activeItemStyles,
|
|
||||||
),
|
|
||||||
'h-9 cursor-pointer px-3',
|
|
||||||
]"
|
|
||||||
:href="item.href"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
v-if="item.icon"
|
|
||||||
:is="item.icon"
|
|
||||||
class="mr-2 h-4 w-4"
|
|
||||||
/>
|
|
||||||
{{ item.title }}
|
|
||||||
</Link>
|
|
||||||
<div
|
|
||||||
v-if="isCurrentUrl(item.href)"
|
|
||||||
class="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"
|
|
||||||
></div>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ml-auto flex items-center space-x-2">
|
|
||||||
<div class="relative flex items-center space-x-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="group h-9 w-9 cursor-pointer"
|
|
||||||
>
|
|
||||||
<Search
|
|
||||||
class="size-5 opacity-80 group-hover:opacity-100"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div class="hidden space-x-1 lg:flex">
|
|
||||||
<template
|
|
||||||
v-for="item in rightNavItems"
|
|
||||||
:key="item.title"
|
|
||||||
>
|
|
||||||
<TooltipProvider :delay-duration="0">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
as-child
|
|
||||||
class="group h-9 w-9 cursor-pointer"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="toUrl(item.href)"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span class="sr-only">{{
|
|
||||||
item.title
|
|
||||||
}}</span>
|
|
||||||
<component
|
|
||||||
:is="item.icon"
|
|
||||||
class="size-5 opacity-80 group-hover:opacity-100"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{{ item.title }}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger :as-child="true">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="focus-within:ring-primary relative size-10 w-auto rounded-full p-1 focus-within:ring-2"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
class="size-8 overflow-hidden rounded-full"
|
|
||||||
>
|
|
||||||
<AvatarImage
|
|
||||||
v-if="auth.user.avatar"
|
|
||||||
:src="auth.user.avatar"
|
|
||||||
:alt="auth.user.name"
|
|
||||||
/>
|
|
||||||
<AvatarFallback
|
|
||||||
class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white"
|
|
||||||
>
|
|
||||||
{{ getInitials(auth.user?.name) }}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" class="w-56">
|
|
||||||
<UserMenuContent :user="auth.user" />
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="ml-auto flex items-center space-x-2">
|
||||||
v-if="props.breadcrumbs.length > 1"
|
<div class="relative flex items-center space-x-1">
|
||||||
class="border-sidebar-border/70 flex w-full border-b"
|
<Button
|
||||||
>
|
variant="ghost"
|
||||||
<div
|
size="icon"
|
||||||
class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl"
|
class="group h-9 w-9 cursor-pointer"
|
||||||
>
|
>
|
||||||
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
<Search class="size-5 opacity-80 group-hover:opacity-100" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div class="hidden space-x-1 lg:flex">
|
||||||
|
<template v-for="item in rightNavItems" :key="item.title">
|
||||||
|
<TooltipProvider :delay-duration="0">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
as-child
|
||||||
|
class="group h-9 w-9 cursor-pointer"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="toUrl(item.href)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ item.title }}</span>
|
||||||
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
class="size-5 opacity-80 group-hover:opacity-100"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{{ item.title }}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger :as-child="true">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="focus-within:ring-primary relative size-10 w-auto rounded-full p-1 focus-within:ring-2"
|
||||||
|
>
|
||||||
|
<Avatar class="size-8 overflow-hidden rounded-full">
|
||||||
|
<AvatarImage
|
||||||
|
v-if="auth.user.avatar"
|
||||||
|
:src="auth.user.avatar"
|
||||||
|
:alt="auth.user.name"
|
||||||
|
/>
|
||||||
|
<AvatarFallback
|
||||||
|
class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white"
|
||||||
|
>
|
||||||
|
{{ getInitials(auth.user?.name) }}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" class="w-56">
|
||||||
|
<UserMenuContent :user="auth.user" />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.breadcrumbs.length > 1"
|
||||||
|
class="border-sidebar-border/70 flex w-full border-b"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl"
|
||||||
|
>
|
||||||
|
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ const name = usePage().props.name;
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-md"
|
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-md"
|
||||||
>
|
>
|
||||||
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
|
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-1 grid flex-1 text-left text-sm">
|
<div class="ml-1 grid flex-1 text-left text-sm">
|
||||||
<span class="mb-0.5 truncate leading-tight font-semibold">{{
|
<span class="mb-0.5 truncate leading-tight font-semibold">{{ name }}</span>
|
||||||
name
|
</div>
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,28 +2,28 @@
|
|||||||
import type { HTMLAttributes } from 'vue';
|
import type { HTMLAttributes } from 'vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: HTMLAttributes['class'];
|
className?: HTMLAttributes['class'];
|
||||||
};
|
};
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 40 42"
|
viewBox="0 0 40 42"
|
||||||
:class="className"
|
:class="className"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-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"
|
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>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import { SidebarProvider } from '@/components/ui/sidebar';
|
|||||||
import type { AppVariant } from '@/types';
|
import type { AppVariant } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
variant?: AppVariant;
|
variant?: AppVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
variant: 'sidebar',
|
variant: 'sidebar',
|
||||||
});
|
});
|
||||||
|
|
||||||
const isOpen = usePage().props.sidebarOpen;
|
const isOpen = usePage().props.sidebarOpen;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="variant === 'header'" class="flex min-h-screen w-full flex-col">
|
<div v-if="variant === 'header'" class="flex min-h-screen w-full flex-col">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<SidebarProvider v-else :default-open="isOpen">
|
<SidebarProvider v-else :default-open="isOpen">
|
||||||
<slot />
|
<slot />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,52 +6,51 @@ import NavFooter from '@/components/NavFooter.vue';
|
|||||||
import NavMain from '@/components/NavMain.vue';
|
import NavMain from '@/components/NavMain.vue';
|
||||||
import NavUser from '@/components/NavUser.vue';
|
import NavUser from '@/components/NavUser.vue';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { dashboard } from '@/routes';
|
import { dashboard } from '@/routes';
|
||||||
import type { NavItem } from '@/types';
|
import type { NavItem } from '@/types';
|
||||||
import teams from "@/routes/teams";
|
import teams from '@/routes/teams';
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
const mainNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Meine Teams',
|
title: 'Meine Teams',
|
||||||
href: teams.index(),
|
href: teams.index(),
|
||||||
icon: UserGroup,
|
icon: UserGroup,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const footerNavItems: NavItem[] = [
|
const footerNavItems: NavItem[] = [];
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Sidebar collapsible="icon" variant="inset">
|
<Sidebar collapsible="icon" variant="inset">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton size="lg" as-child>
|
<SidebarMenuButton size="lg" as-child>
|
||||||
<Link :href="dashboard()">
|
<Link :href="dashboard()">
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain :items="mainNavItems" />
|
<NavMain :items="mainNavItems" />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavFooter :items="footerNavItems" />
|
<NavFooter :items="footerNavItems" />
|
||||||
<NavUser />
|
<NavUser />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<slot />
|
<slot />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ import { SidebarTrigger } from '@/components/ui/sidebar';
|
|||||||
import type { BreadcrumbItem } from '@/types';
|
import type { BreadcrumbItem } from '@/types';
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
breadcrumbs?: BreadcrumbItem[];
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
breadcrumbs: () => [],
|
breadcrumbs: () => [],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header
|
<header
|
||||||
class="border-sidebar-border/70 flex h-16 shrink-0 items-center gap-2 border-b px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4"
|
class="border-sidebar-border/70 flex h-16 shrink-0 items-center gap-2 border-b px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<SidebarTrigger class="-ml-1" />
|
<SidebarTrigger class="-ml-1" />
|
||||||
<template v-if="breadcrumbs && breadcrumbs.length > 0">
|
<template v-if="breadcrumbs && breadcrumbs.length > 0">
|
||||||
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,29 +5,29 @@ import { useAppearance } from '@/composables/useAppearance';
|
|||||||
const { appearance, updateAppearance } = useAppearance();
|
const { appearance, updateAppearance } = useAppearance();
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ value: 'light', Icon: Sun, label: 'Light' },
|
{ value: 'light', Icon: Sun, label: 'Light' },
|
||||||
{ value: 'dark', Icon: Moon, label: 'Dark' },
|
{ value: 'dark', Icon: Moon, label: 'Dark' },
|
||||||
{ value: 'system', Icon: Monitor, label: 'System' },
|
{ value: 'system', Icon: Monitor, label: 'System' },
|
||||||
] as const;
|
] as const;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800"
|
class="inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="{ value, Icon, label } in tabs"
|
||||||
|
:key="value"
|
||||||
|
@click="updateAppearance(value)"
|
||||||
|
:class="[
|
||||||
|
'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
|
||||||
|
appearance === value
|
||||||
|
? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
|
||||||
|
: 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60',
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<button
|
<component :is="Icon" class="-ml-1 h-4 w-4" />
|
||||||
v-for="{ value, Icon, label } in tabs"
|
<span class="ml-1.5 text-sm">{{ label }}</span>
|
||||||
:key="value"
|
</button>
|
||||||
@click="updateAppearance(value)"
|
</div>
|
||||||
:class="[
|
|
||||||
'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
|
|
||||||
appearance === value
|
|
||||||
? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
|
|
||||||
: 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<component :is="Icon" class="-ml-1 h-4 w-4" />
|
|
||||||
<span class="ml-1.5 text-sm">{{ label }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link } from '@inertiajs/vue3';
|
import { Link } from '@inertiajs/vue3';
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from '@/components/ui/breadcrumb';
|
} from '@/components/ui/breadcrumb';
|
||||||
import type { BreadcrumbItem as BreadcrumbItemType } from '@/types';
|
import type { BreadcrumbItem as BreadcrumbItemType } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
breadcrumbs: BreadcrumbItemType[];
|
breadcrumbs: BreadcrumbItemType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<template v-for="(item, index) in breadcrumbs" :key="index">
|
<template v-for="(item, index) in breadcrumbs" :key="index">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<template v-if="index === breadcrumbs.length - 1">
|
<template v-if="index === breadcrumbs.length - 1">
|
||||||
<BreadcrumbPage>{{ item.title }}</BreadcrumbPage>
|
<BreadcrumbPage>{{ item.title }}</BreadcrumbPage>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<BreadcrumbLink as-child>
|
<BreadcrumbLink as-child>
|
||||||
<Link :href="item.href">{{ item.title }}</Link>
|
<Link :href="item.href">{{ item.title }}</Link>
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</template>
|
</template>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator v-if="index !== breadcrumbs.length - 1" />
|
<BreadcrumbSeparator v-if="index !== breadcrumbs.length - 1" />
|
||||||
</template>
|
</template>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import InputError from '@/components/InputError.vue';
|
|||||||
import PasswordInput from '@/components/PasswordInput.vue';
|
import PasswordInput from '@/components/PasswordInput.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
@@ -22,92 +22,87 @@ const passwordInput = useTemplateRef('passwordInput');
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<Heading
|
<Heading
|
||||||
variant="small"
|
variant="small"
|
||||||
title="Delete account"
|
title="Delete account"
|
||||||
description="Delete your account and all of its resources"
|
description="Delete your account and all of its resources"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10"
|
class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10"
|
||||||
>
|
>
|
||||||
<div class="relative space-y-0.5 text-red-600 dark:text-red-100">
|
<div class="relative space-y-0.5 text-red-600 dark:text-red-100">
|
||||||
<p class="font-medium">Warning</p>
|
<p class="font-medium">Warning</p>
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
Please proceed with caution, this cannot be undone.
|
Please proceed with caution, this cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger as-child>
|
||||||
|
<Button variant="destructive" data-test="delete-user-button"
|
||||||
|
>Delete account</Button
|
||||||
|
>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<Form
|
||||||
|
v-bind="ProfileController.destroy.form()"
|
||||||
|
reset-on-success
|
||||||
|
@error="() => passwordInput?.focus()"
|
||||||
|
:options="{
|
||||||
|
preserveScroll: true,
|
||||||
|
}"
|
||||||
|
class="space-y-6"
|
||||||
|
v-slot="{ errors, processing, reset, clearErrors }"
|
||||||
|
>
|
||||||
|
<DialogHeader class="space-y-3">
|
||||||
|
<DialogTitle
|
||||||
|
>Are you sure you want to delete your account?</DialogTitle
|
||||||
|
>
|
||||||
|
<DialogDescription>
|
||||||
|
Once your account is deleted, all of its resources and data will
|
||||||
|
also be permanently deleted. Please enter your password to
|
||||||
|
confirm you would like to permanently delete your account.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="password" class="sr-only">Password</Label>
|
||||||
|
<PasswordInput
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
ref="passwordInput"
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
<InputError :message="errors.password" />
|
||||||
</div>
|
</div>
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger as-child>
|
|
||||||
<Button variant="destructive" data-test="delete-user-button"
|
|
||||||
>Delete account</Button
|
|
||||||
>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<Form
|
|
||||||
v-bind="ProfileController.destroy.form()"
|
|
||||||
reset-on-success
|
|
||||||
@error="() => passwordInput?.focus()"
|
|
||||||
:options="{
|
|
||||||
preserveScroll: true,
|
|
||||||
}"
|
|
||||||
class="space-y-6"
|
|
||||||
v-slot="{ errors, processing, reset, clearErrors }"
|
|
||||||
>
|
|
||||||
<DialogHeader class="space-y-3">
|
|
||||||
<DialogTitle
|
|
||||||
>Are you sure you want to delete your
|
|
||||||
account?</DialogTitle
|
|
||||||
>
|
|
||||||
<DialogDescription>
|
|
||||||
Once your account is deleted, all of its
|
|
||||||
resources and data will also be permanently
|
|
||||||
deleted. Please enter your password to confirm
|
|
||||||
you would like to permanently delete your
|
|
||||||
account.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<DialogFooter class="gap-2">
|
||||||
<Label for="password" class="sr-only"
|
<DialogClose as-child>
|
||||||
>Password</Label
|
<Button
|
||||||
>
|
variant="secondary"
|
||||||
<PasswordInput
|
@click="
|
||||||
id="password"
|
() => {
|
||||||
name="password"
|
clearErrors();
|
||||||
ref="passwordInput"
|
reset();
|
||||||
placeholder="Password"
|
}
|
||||||
/>
|
"
|
||||||
<InputError :message="errors.password" />
|
>
|
||||||
</div>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
<DialogFooter class="gap-2">
|
<Button
|
||||||
<DialogClose as-child>
|
type="submit"
|
||||||
<Button
|
variant="destructive"
|
||||||
variant="secondary"
|
:disabled="processing"
|
||||||
@click="
|
data-test="confirm-delete-user-button"
|
||||||
() => {
|
>
|
||||||
clearErrors();
|
Delete account
|
||||||
reset();
|
</Button>
|
||||||
}
|
</DialogFooter>
|
||||||
"
|
</Form>
|
||||||
>
|
</DialogContent>
|
||||||
Cancel
|
</Dialog>
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
:disabled="processing"
|
|
||||||
data-test="confirm-delete-user-button"
|
|
||||||
>
|
|
||||||
Delete account
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
variant?: 'default' | 'small';
|
variant?: 'default' | 'small';
|
||||||
};
|
};
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header :class="variant === 'small' ? '' : 'mb-8 space-y-0.5'">
|
<header :class="variant === 'small' ? '' : 'mb-8 space-y-0.5'">
|
||||||
<h2
|
<h2
|
||||||
:class="
|
:class="
|
||||||
variant === 'small'
|
variant === 'small'
|
||||||
? 'mb-0.5 text-base font-medium'
|
? 'mb-0.5 text-base font-medium'
|
||||||
: 'text-xl font-semibold tracking-tight'
|
: 'text-xl font-semibold tracking-tight'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h2>
|
</h2>
|
||||||
<p v-if="description" class="text-muted-foreground text-sm">
|
<p v-if="description" class="text-muted-foreground text-sm">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
message?: string;
|
message?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-show="message">
|
<div v-show="message">
|
||||||
<p class="text-sm text-red-600 dark:text-red-500">
|
<p class="text-sm text-red-600 dark:text-red-500">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,58 +8,58 @@ import PasskeyRegister from '@/components/PasskeyRegister.vue';
|
|||||||
import { destroy } from '@/actions/Laravel/Passkeys/Http/Controllers/PasskeyRegistrationController';
|
import { destroy } from '@/actions/Laravel/Passkeys/Http/Controllers/PasskeyRegistrationController';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
canManagePasskeys?: boolean;
|
canManagePasskeys?: boolean;
|
||||||
passkeys?: Passkey[];
|
passkeys?: Passkey[];
|
||||||
};
|
};
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
canManagePasskeys: false,
|
canManagePasskeys: false,
|
||||||
passkeys: () => [],
|
passkeys: () => [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = (id: number, onError: () => void) => {
|
const handleDelete = (id: number, onError: () => void) => {
|
||||||
router.delete(destroy.url(id), {
|
router.delete(destroy.url(id), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onError,
|
onError,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegisterSuccess = () => {
|
const handleRegisterSuccess = () => {
|
||||||
router.reload();
|
router.reload();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="canManagePasskeys" class="space-y-6">
|
<div v-if="canManagePasskeys" class="space-y-6">
|
||||||
<Heading
|
<Heading
|
||||||
variant="small"
|
variant="small"
|
||||||
title="Passkeys"
|
title="Passkeys"
|
||||||
description="Manage your passkeys for passwordless sign-in"
|
description="Manage your passkeys for passwordless sign-in"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="border-border overflow-hidden rounded-lg border">
|
||||||
|
<template v-if="passkeys.length">
|
||||||
|
<PasskeyItem
|
||||||
|
v-for="passkey in passkeys"
|
||||||
|
:key="passkey.id"
|
||||||
|
:passkey="passkey"
|
||||||
|
@remove="handleDelete"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="border-border overflow-hidden rounded-lg border">
|
<div v-else class="p-8 text-center">
|
||||||
<template v-if="passkeys.length">
|
<div
|
||||||
<PasskeyItem
|
class="bg-muted mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl"
|
||||||
v-for="passkey in passkeys"
|
>
|
||||||
:key="passkey.id"
|
<KeyRound class="text-muted-foreground h-7 w-7" />
|
||||||
:passkey="passkey"
|
|
||||||
@remove="handleDelete"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-else class="p-8 text-center">
|
|
||||||
<div
|
|
||||||
class="bg-muted mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl"
|
|
||||||
>
|
|
||||||
<KeyRound class="text-muted-foreground h-7 w-7" />
|
|
||||||
</div>
|
|
||||||
<p class="font-medium">No passkeys yet</p>
|
|
||||||
<p class="text-muted-foreground mt-1 text-sm">
|
|
||||||
Add a passkey to sign in without a password
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="font-medium">No passkeys yet</p>
|
||||||
<PasskeyRegister @success="handleRegisterSuccess" />
|
<p class="text-muted-foreground mt-1 text-sm">
|
||||||
|
Add a passkey to sign in without a password
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PasskeyRegister @success="handleRegisterSuccess" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
|
|||||||
import { disable, enable } from '@/routes/two-factor';
|
import { disable, enable } from '@/routes/two-factor';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
canManageTwoFactor?: boolean;
|
canManageTwoFactor?: boolean;
|
||||||
requiresConfirmation?: boolean;
|
requiresConfirmation?: boolean;
|
||||||
twoFactorEnabled?: boolean;
|
twoFactorEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
canManageTwoFactor: false,
|
canManageTwoFactor: false,
|
||||||
requiresConfirmation: false,
|
requiresConfirmation: false,
|
||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { hasSetupData, clearTwoFactorAuthData } = useTwoFactorAuth();
|
const { hasSetupData, clearTwoFactorAuthData } = useTwoFactorAuth();
|
||||||
@@ -28,66 +28,59 @@ onUnmounted(() => clearTwoFactorAuthData());
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="canManageTwoFactor" class="space-y-6">
|
<div v-if="canManageTwoFactor" class="space-y-6">
|
||||||
<Heading
|
<Heading
|
||||||
variant="small"
|
variant="small"
|
||||||
title="Two-factor authentication"
|
title="Two-factor authentication"
|
||||||
description="Manage your two-factor authentication settings"
|
description="Manage your two-factor authentication settings"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!twoFactorEnabled"
|
v-if="!twoFactorEnabled"
|
||||||
class="flex flex-col items-start justify-start space-y-4"
|
class="flex flex-col items-start justify-start space-y-4"
|
||||||
|
>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button v-if="hasSetupData" @click="showSetupModal = true">
|
||||||
|
<ShieldCheck />Continue setup
|
||||||
|
</Button>
|
||||||
|
<Form
|
||||||
|
v-else
|
||||||
|
v-bind="enable.form()"
|
||||||
|
@success="showSetupModal = true"
|
||||||
|
#default="{ processing }"
|
||||||
>
|
>
|
||||||
<p class="text-muted-foreground text-sm">
|
<Button type="submit" :disabled="processing"> Enable 2FA </Button>
|
||||||
When you enable two-factor authentication, you will be prompted
|
</Form>
|
||||||
for a secure pin during login. This pin can be retrieved from a
|
</div>
|
||||||
TOTP-supported application on your phone.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button v-if="hasSetupData" @click="showSetupModal = true">
|
|
||||||
<ShieldCheck />Continue setup
|
|
||||||
</Button>
|
|
||||||
<Form
|
|
||||||
v-else
|
|
||||||
v-bind="enable.form()"
|
|
||||||
@success="showSetupModal = true"
|
|
||||||
#default="{ processing }"
|
|
||||||
>
|
|
||||||
<Button type="submit" :disabled="processing">
|
|
||||||
Enable 2FA
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="flex flex-col items-start justify-start space-y-4">
|
|
||||||
<p class="text-muted-foreground text-sm">
|
|
||||||
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 class="relative inline">
|
|
||||||
<Form v-bind="disable.form()" #default="{ processing }">
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
type="submit"
|
|
||||||
:disabled="processing"
|
|
||||||
>
|
|
||||||
Disable 2FA
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TwoFactorRecoveryCodes />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TwoFactorSetupModal
|
|
||||||
v-model:isOpen="showSetupModal"
|
|
||||||
:requiresConfirmation="requiresConfirmation"
|
|
||||||
:twoFactorEnabled="twoFactorEnabled"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col items-start justify-start space-y-4">
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
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 class="relative inline">
|
||||||
|
<Form v-bind="disable.form()" #default="{ processing }">
|
||||||
|
<Button variant="destructive" type="submit" :disabled="processing">
|
||||||
|
Disable 2FA
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TwoFactorRecoveryCodes />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TwoFactorSetupModal
|
||||||
|
v-model:isOpen="showSetupModal"
|
||||||
|
:requiresConfirmation="requiresConfirmation"
|
||||||
|
:twoFactorEnabled="twoFactorEnabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { toUrl } from '@/lib/utils';
|
import { toUrl } from '@/lib/utils';
|
||||||
import type { NavItem } from '@/types';
|
import type { NavItem } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
items: NavItem[];
|
items: NavItem[];
|
||||||
class?: string;
|
class?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarGroup
|
<SidebarGroup
|
||||||
:class="`group-data-[collapsible=icon]:p-0 ${$props.class || ''}`"
|
:class="`group-data-[collapsible=icon]:p-0 ${$props.class || ''}`"
|
||||||
>
|
>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem v-for="item in items" :key="item.title">
|
<SidebarMenuItem v-for="item in items" :key="item.title">
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100"
|
class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100"
|
||||||
as-child
|
as-child
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
:href="toUrl(item.href)"
|
:href="toUrl(item.href)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<component :is="item.icon" />
|
<component :is="item.icon" />
|
||||||
<span>{{ item.title }}</span>
|
<span>{{ item.title }}</span>
|
||||||
</a>
|
</a>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link } from '@inertiajs/vue3';
|
import { Link } from '@inertiajs/vue3';
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
||||||
import type { NavItem } from '@/types';
|
import type { NavItem } from '@/types';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
items: NavItem[];
|
items: NavItem[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { isCurrentUrl } = useCurrentUrl();
|
const { isCurrentUrl } = useCurrentUrl();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarGroup class="px-2 py-0">
|
<SidebarGroup class="px-2 py-0">
|
||||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem v-for="item in items" :key="item.title">
|
<SidebarMenuItem v-for="item in items" :key="item.title">
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
as-child
|
as-child
|
||||||
:is-active="isCurrentUrl(item.href)"
|
:is-active="isCurrentUrl(item.href)"
|
||||||
:tooltip="item.title"
|
:tooltip="item.title"
|
||||||
>
|
>
|
||||||
<Link :href="item.href">
|
<Link :href="item.href">
|
||||||
<component :is="item.icon" />
|
<component :is="item.icon" />
|
||||||
<span>{{ item.title }}</span>
|
<span>{{ item.title }}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { usePage } from '@inertiajs/vue3';
|
|||||||
import { ChevronsUpDown } from '@lucide/vue';
|
import { ChevronsUpDown } from '@lucide/vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import UserInfo from '@/components/UserInfo.vue';
|
import UserInfo from '@/components/UserInfo.vue';
|
||||||
import UserMenuContent from '@/components/UserMenuContent.vue';
|
import UserMenuContent from '@/components/UserMenuContent.vue';
|
||||||
@@ -22,34 +22,30 @@ const { isMobile, state } = useSidebar();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
size="lg"
|
||||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
data-test="sidebar-menu-button"
|
data-test="sidebar-menu-button"
|
||||||
>
|
>
|
||||||
<UserInfo :user="user" />
|
<UserInfo :user="user" />
|
||||||
<ChevronsUpDown class="ml-auto size-4" />
|
<ChevronsUpDown class="ml-auto size-4" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||||
:side="
|
:side="
|
||||||
isMobile
|
isMobile ? 'bottom' : state === 'collapsed' ? 'left' : 'bottom'
|
||||||
? 'bottom'
|
"
|
||||||
: state === 'collapsed'
|
align="end"
|
||||||
? 'left'
|
:side-offset="4"
|
||||||
: 'bottom'
|
>
|
||||||
"
|
<UserMenuContent :user="user" />
|
||||||
align="end"
|
</DropdownMenuContent>
|
||||||
:side-offset="4"
|
</DropdownMenu>
|
||||||
>
|
</SidebarMenuItem>
|
||||||
<UserMenuContent :user="user" />
|
</SidebarMenu>
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,93 +3,93 @@ import { KeyRound, Trash2 } from '@lucide/vue';
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import type { Passkey } from '@/types/auth';
|
import type { Passkey } from '@/types/auth';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
passkey: Passkey;
|
passkey: Passkey;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
remove: [id: number, onError: () => void];
|
remove: [id: number, onError: () => void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isDeleting = ref(false);
|
const isDeleting = ref(false);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
isDeleting.value = true;
|
isDeleting.value = true;
|
||||||
emit('remove', props.passkey.id, () => {
|
emit('remove', props.passkey.id, () => {
|
||||||
isDeleting.value = false;
|
isDeleting.value = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between border-b p-4 last:border-b-0">
|
<div class="flex items-center justify-between border-b p-4 last:border-b-0">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
class="bg-muted flex h-10 w-10 shrink-0 items-center justify-center rounded-xl"
|
class="bg-muted flex h-10 w-10 shrink-0 items-center justify-center rounded-xl"
|
||||||
>
|
>
|
||||||
<KeyRound class="text-muted-foreground h-5 w-5" />
|
<KeyRound class="text-muted-foreground h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<p class="font-medium tracking-tight">{{ passkey.name }}</p>
|
<p class="font-medium tracking-tight">{{ passkey.name }}</p>
|
||||||
<span
|
<span
|
||||||
v-if="passkey.authenticator"
|
v-if="passkey.authenticator"
|
||||||
class="bg-muted text-muted-foreground ring-border inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-medium tracking-wide uppercase ring-1 ring-inset"
|
class="bg-muted text-muted-foreground ring-border inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-medium tracking-wide uppercase ring-1 ring-inset"
|
||||||
>
|
>
|
||||||
{{ passkey.authenticator }}
|
{{ passkey.authenticator }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<p class="text-muted-foreground text-sm">
|
|
||||||
Added {{ passkey.created_at_diff }}
|
|
||||||
<template v-if="passkey.last_used_at_diff">
|
|
||||||
<span class="text-muted-foreground/50 mx-1">/</span>
|
|
||||||
Last used {{ passkey.last_used_at_diff }}
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
<Dialog>
|
Added {{ passkey.created_at_diff }}
|
||||||
<DialogTrigger as-child>
|
<template v-if="passkey.last_used_at_diff">
|
||||||
<Button
|
<span class="text-muted-foreground/50 mx-1">/</span>
|
||||||
variant="ghost"
|
Last used {{ passkey.last_used_at_diff }}
|
||||||
size="sm"
|
</template>
|
||||||
class="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
</p>
|
||||||
>
|
</div>
|
||||||
<Trash2 class="h-4 w-4" />
|
|
||||||
<span class="sr-only">Remove</span>
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle>Remove passkey</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Are you sure you want to remove the "{{ passkey.name }}"
|
|
||||||
passkey? You will no longer be able to use it to sign in.
|
|
||||||
</DialogDescription>
|
|
||||||
<DialogFooter class="gap-2">
|
|
||||||
<DialogClose as-child>
|
|
||||||
<Button variant="secondary">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
:disabled="isDeleting"
|
|
||||||
@click="handleDelete"
|
|
||||||
>
|
|
||||||
{{ isDeleting ? 'Removing...' : 'Remove passkey' }}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-4 w-4" />
|
||||||
|
<span class="sr-only">Remove</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>Remove passkey</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to remove the "{{ passkey.name }}" passkey? You
|
||||||
|
will no longer be able to use it to sign in.
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter class="gap-2">
|
||||||
|
<DialogClose as-child>
|
||||||
|
<Button variant="secondary">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
:disabled="isDeleting"
|
||||||
|
@click="handleDelete"
|
||||||
|
>
|
||||||
|
{{ isDeleting ? 'Removing...' : 'Remove passkey' }}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,96 +7,96 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
success: [];
|
success: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const getDefaultPasskeyName = () => {
|
const getDefaultPasskeyName = () => {
|
||||||
const ua = navigator.userAgent;
|
const ua = navigator.userAgent;
|
||||||
|
|
||||||
const browser = [
|
const browser = [
|
||||||
{ pattern: /Edg|Edge/, name: 'Edge' },
|
{ pattern: /Edg|Edge/, name: 'Edge' },
|
||||||
{ pattern: /OPR|Opera|OPiOS/, name: 'Opera' },
|
{ pattern: /OPR|Opera|OPiOS/, name: 'Opera' },
|
||||||
{ pattern: /Firefox|FxiOS/, name: 'Firefox' },
|
{ pattern: /Firefox|FxiOS/, name: 'Firefox' },
|
||||||
{ pattern: /Chrome|CriOS/, name: 'Chrome' },
|
{ pattern: /Chrome|CriOS/, name: 'Chrome' },
|
||||||
{ pattern: /Safari/, name: 'Safari' },
|
{ pattern: /Safari/, name: 'Safari' },
|
||||||
].find(({ pattern }) => pattern.test(ua))?.name;
|
].find(({ pattern }) => pattern.test(ua))?.name;
|
||||||
|
|
||||||
const os = [
|
const os = [
|
||||||
{ pattern: /iPhone/, name: 'iPhone' },
|
{ pattern: /iPhone/, name: 'iPhone' },
|
||||||
{ pattern: /iPad|Macintosh(?=.*Mobile)/, name: 'iPad' },
|
{ pattern: /iPad|Macintosh(?=.*Mobile)/, name: 'iPad' },
|
||||||
{ pattern: /Android/, name: 'Android' },
|
{ pattern: /Android/, name: 'Android' },
|
||||||
{ pattern: /Mac/, name: 'Mac' },
|
{ pattern: /Mac/, name: 'Mac' },
|
||||||
{ pattern: /Windows/, name: 'Windows' },
|
{ pattern: /Windows/, name: 'Windows' },
|
||||||
].find(({ pattern }) => pattern.test(ua))?.name;
|
].find(({ pattern }) => pattern.test(ua))?.name;
|
||||||
|
|
||||||
return [browser, os].filter(Boolean).join(' on ') || '';
|
return [browser, os].filter(Boolean).join(' on ') || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const name = ref(getDefaultPasskeyName());
|
const name = ref(getDefaultPasskeyName());
|
||||||
const showForm = ref(false);
|
const showForm = ref(false);
|
||||||
|
|
||||||
const { register, isLoading, error, isSupported } = usePasskeyRegister({
|
const { register, isLoading, error, isSupported } = usePasskeyRegister({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
name.value = '';
|
name.value = '';
|
||||||
showForm.value = false;
|
showForm.value = false;
|
||||||
emit('success');
|
emit('success');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!name.value.trim()) {
|
if (!name.value.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await register(name.value);
|
await register(name.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
showForm.value = false;
|
showForm.value = false;
|
||||||
name.value = '';
|
name.value = '';
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isSupported" class="text-muted-foreground text-sm">
|
<div v-if="!isSupported" class="text-muted-foreground text-sm">
|
||||||
Passkeys are not supported in this browser.
|
Passkeys are not supported in this browser.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button v-else-if="!showForm" variant="outline" @click="showForm = true">
|
||||||
|
Add passkey
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<form
|
||||||
|
v-else
|
||||||
|
@submit="handleSubmit"
|
||||||
|
class="border-border bg-muted/50 space-y-4 rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="passkey-name">Passkey name</Label>
|
||||||
|
<Input
|
||||||
|
id="passkey-name"
|
||||||
|
type="text"
|
||||||
|
v-model="name"
|
||||||
|
placeholder="e.g., MacBook Pro, iPhone"
|
||||||
|
class="border-foreground/20 mt-1 block w-full"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<p class="text-muted-foreground text-xs">
|
||||||
|
A name helps you identify this passkey later.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button v-else-if="!showForm" variant="outline" @click="showForm = true">
|
<InputError v-if="error" :message="error" />
|
||||||
Add passkey
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<form
|
<div class="flex gap-2">
|
||||||
v-else
|
<Button type="submit" :disabled="isLoading || !name.trim()">
|
||||||
@submit="handleSubmit"
|
{{ isLoading ? 'Registering...' : 'Register passkey' }}
|
||||||
class="border-border bg-muted/50 space-y-4 rounded-lg border p-4"
|
</Button>
|
||||||
>
|
<Button type="button" variant="ghost" @click="handleCancel">
|
||||||
<div class="grid gap-2">
|
Cancel
|
||||||
<Label for="passkey-name">Passkey name</Label>
|
</Button>
|
||||||
<Input
|
</div>
|
||||||
id="passkey-name"
|
</form>
|
||||||
type="text"
|
|
||||||
v-model="name"
|
|
||||||
placeholder="e.g., MacBook Pro, iPhone"
|
|
||||||
class="border-foreground/20 mt-1 block w-full"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
<p class="text-muted-foreground text-xs">
|
|
||||||
A name helps you identify this passkey later.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<InputError v-if="error" :message="error" />
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Button type="submit" :disabled="isLoading || !name.trim()">
|
|
||||||
{{ isLoading ? 'Registering...' : 'Register passkey' }}
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" @click="handleCancel">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,65 +9,65 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
routes?: {
|
routes?: {
|
||||||
options: UrlMethodPair;
|
options: UrlMethodPair;
|
||||||
submit: UrlMethodPair;
|
submit: UrlMethodPair;
|
||||||
};
|
};
|
||||||
label?: string;
|
label?: string;
|
||||||
loadingLabel?: string;
|
loadingLabel?: string;
|
||||||
separator?: string;
|
separator?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const { verify, isLoading, error, isSupported } = usePasskeyVerify({
|
const { verify, isLoading, error, isSupported } = usePasskeyVerify({
|
||||||
...(props.routes
|
...(props.routes
|
||||||
? {
|
? {
|
||||||
routes: {
|
routes: {
|
||||||
options: props.routes.options.url,
|
options: props.routes.options.url,
|
||||||
submit: props.routes.submit.url,
|
submit: props.routes.submit.url,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
router.visit(response.redirect ?? '/dashboard');
|
router.visit(response.redirect ?? '/dashboard');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isSupported">
|
<div v-if="isSupported">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@click="verify"
|
@click="verify"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
>
|
>
|
||||||
<Spinner v-if="isLoading" />
|
<Spinner v-if="isLoading" />
|
||||||
<KeyRound v-else class="h-4 w-4" />
|
<KeyRound v-else class="h-4 w-4" />
|
||||||
{{
|
{{
|
||||||
isLoading
|
isLoading
|
||||||
? (props.loadingLabel ?? 'Authenticating...')
|
? (props.loadingLabel ?? 'Authenticating...')
|
||||||
: (props.label ?? 'Sign in with a passkey')
|
: (props.label ?? 'Sign in with a passkey')
|
||||||
}}
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div v-if="error" class="text-center">
|
<div v-if="error" class="text-center">
|
||||||
<InputError :message="error" />
|
<InputError :message="error" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative my-6">
|
|
||||||
<div class="absolute inset-0 flex items-center">
|
|
||||||
<Separator class="w-full" />
|
|
||||||
</div>
|
|
||||||
<div class="relative flex justify-center text-xs uppercase">
|
|
||||||
<span class="bg-background text-muted-foreground px-2">
|
|
||||||
{{ props.separator ?? 'Or continue with email' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="relative my-6">
|
||||||
|
<div class="absolute inset-0 flex items-center">
|
||||||
|
<Separator class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center text-xs uppercase">
|
||||||
|
<span class="bg-background text-muted-foreground px-2">
|
||||||
|
{{ props.separator ?? 'Or continue with email' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,39 +8,39 @@ import { cn } from '@/lib/utils';
|
|||||||
defineOptions({ inheritAttrs: false });
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes['class'];
|
class?: HTMLAttributes['class'];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
const inputRef = useTemplateRef('inputRef');
|
const inputRef = useTemplateRef('inputRef');
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
$el: inputRef,
|
$el: inputRef,
|
||||||
focus: () => inputRef.value?.$el?.focus(),
|
focus: () => inputRef.value?.$el?.focus(),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Input
|
<Input
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
:type="showPassword ? 'text' : 'password'"
|
:type="showPassword ? 'text' : 'password'"
|
||||||
:class="cn('pr-10', props.class)"
|
:class="cn('pr-10', props.class)"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="showPassword = !showPassword"
|
@click="showPassword = !showPassword"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute inset-y-0 right-0 flex items-center rounded-r-md px-3 focus-visible:ring-[3px] focus-visible:outline-none',
|
'text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute inset-y-0 right-0 flex items-center rounded-r-md px-3 focus-visible:ring-[3px] focus-visible:outline-none',
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
:aria-label="showPassword ? 'Hide password' : 'Show password'"
|
:aria-label="showPassword ? 'Hide password' : 'Show password'"
|
||||||
:tabindex="-1"
|
:tabindex="-1"
|
||||||
>
|
>
|
||||||
<EyeOff v-if="showPassword" class="size-4" />
|
<EyeOff v-if="showPassword" class="size-4" />
|
||||||
<Eye v-else class="size-4" />
|
<Eye v-else class="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,27 +5,27 @@ const patternId = `pattern-${useId()}`;
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
class="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20"
|
class="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20"
|
||||||
fill="none"
|
fill="none"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<pattern
|
<pattern
|
||||||
:id="patternId"
|
:id="patternId"
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="0"
|
||||||
width="8"
|
width="8"
|
||||||
height="8"
|
height="8"
|
||||||
patternUnits="userSpaceOnUse"
|
patternUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
|
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect
|
<rect
|
||||||
stroke="none"
|
stroke="none"
|
||||||
:fill="`url(#${patternId})`"
|
:fill="`url(#${patternId})`"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
></rect>
|
></rect>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,23 +3,23 @@ import type { LinkComponentBaseProps, Method } from '@inertiajs/core';
|
|||||||
import { Link } from '@inertiajs/vue3';
|
import { Link } from '@inertiajs/vue3';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
href: LinkComponentBaseProps['href'];
|
href: LinkComponentBaseProps['href'];
|
||||||
tabindex?: number;
|
tabindex?: number;
|
||||||
method?: Method;
|
method?: Method;
|
||||||
as?: string;
|
as?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Link
|
<Link
|
||||||
:href="href"
|
:href="href"
|
||||||
:tabindex="tabindex"
|
:tabindex="tabindex"
|
||||||
:method="method"
|
:method="method"
|
||||||
:as="as"
|
:as="as"
|
||||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</Link>
|
</Link>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
|||||||
import AlertError from '@/components/AlertError.vue';
|
import AlertError from '@/components/AlertError.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
|
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
|
||||||
import { regenerateRecoveryCodes } from '@/routes/two-factor';
|
import { regenerateRecoveryCodes } from '@/routes/two-factor';
|
||||||
@@ -19,105 +19,93 @@ const isRecoveryCodesVisible = ref<boolean>(false);
|
|||||||
const recoveryCodeSectionRef = useTemplateRef('recoveryCodeSectionRef');
|
const recoveryCodeSectionRef = useTemplateRef('recoveryCodeSectionRef');
|
||||||
|
|
||||||
const toggleRecoveryCodesVisibility = async () => {
|
const toggleRecoveryCodesVisibility = async () => {
|
||||||
if (!isRecoveryCodesVisible.value && !recoveryCodesList.value.length) {
|
if (!isRecoveryCodesVisible.value && !recoveryCodesList.value.length) {
|
||||||
await fetchRecoveryCodes();
|
await fetchRecoveryCodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
isRecoveryCodesVisible.value = !isRecoveryCodesVisible.value;
|
isRecoveryCodesVisible.value = !isRecoveryCodesVisible.value;
|
||||||
|
|
||||||
if (isRecoveryCodesVisible.value) {
|
if (isRecoveryCodesVisible.value) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
recoveryCodeSectionRef.value?.scrollIntoView({ behavior: 'smooth' });
|
recoveryCodeSectionRef.value?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!recoveryCodesList.value.length) {
|
if (!recoveryCodesList.value.length) {
|
||||||
await fetchRecoveryCodes();
|
await fetchRecoveryCodes();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card class="w-full">
|
<Card class="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex gap-3">
|
<CardTitle class="flex gap-3">
|
||||||
<LockKeyhole class="size-4" />2FA recovery codes
|
<LockKeyhole class="size-4" />2FA recovery codes
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Recovery codes let you regain access if you lose your 2FA
|
Recovery codes let you regain access if you lose your 2FA device. Store
|
||||||
device. Store them in a secure password manager.
|
them in a secure password manager.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-3 select-none sm:flex-row sm:items-center sm:justify-between"
|
class="flex flex-col gap-3 select-none sm:flex-row sm:items-center sm:justify-between"
|
||||||
>
|
>
|
||||||
<Button @click="toggleRecoveryCodesVisibility" class="w-fit">
|
<Button @click="toggleRecoveryCodesVisibility" class="w-fit">
|
||||||
<component
|
<component
|
||||||
:is="isRecoveryCodesVisible ? EyeOff : Eye"
|
:is="isRecoveryCodesVisible ? EyeOff : Eye"
|
||||||
class="size-4"
|
class="size-4"
|
||||||
/>
|
/>
|
||||||
{{ isRecoveryCodesVisible ? 'Hide' : 'View' }} recovery
|
{{ isRecoveryCodesVisible ? 'Hide' : 'View' }} recovery codes
|
||||||
codes
|
</Button>
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
v-if="isRecoveryCodesVisible && recoveryCodesList.length"
|
v-if="isRecoveryCodesVisible && recoveryCodesList.length"
|
||||||
v-bind="regenerateRecoveryCodes.form()"
|
v-bind="regenerateRecoveryCodes.form()"
|
||||||
method="post"
|
method="post"
|
||||||
:options="{ preserveScroll: true }"
|
:options="{ preserveScroll: true }"
|
||||||
@success="fetchRecoveryCodes"
|
@success="fetchRecoveryCodes"
|
||||||
#default="{ processing }"
|
#default="{ processing }"
|
||||||
>
|
>
|
||||||
<Button
|
<Button variant="secondary" type="submit" :disabled="processing">
|
||||||
variant="secondary"
|
<RefreshCw /> Regenerate codes
|
||||||
type="submit"
|
</Button>
|
||||||
:disabled="processing"
|
</Form>
|
||||||
>
|
</div>
|
||||||
<RefreshCw /> Regenerate codes
|
<div
|
||||||
</Button>
|
:class="[
|
||||||
</Form>
|
'relative overflow-hidden transition-all duration-300',
|
||||||
|
isRecoveryCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div v-if="errors?.length" class="mt-6">
|
||||||
|
<AlertError :errors="errors" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-3 space-y-3">
|
||||||
|
<div
|
||||||
|
ref="recoveryCodeSectionRef"
|
||||||
|
class="bg-muted grid gap-1 rounded-lg p-4 font-mono text-sm"
|
||||||
|
>
|
||||||
|
<div v-if="!recoveryCodesList.length" class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="n in 8"
|
||||||
|
:key="n"
|
||||||
|
class="bg-muted-foreground/20 h-4 animate-pulse rounded"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else v-for="(code, index) in recoveryCodesList" :key="index">
|
||||||
:class="[
|
{{ code }}
|
||||||
'relative overflow-hidden transition-all duration-300',
|
|
||||||
isRecoveryCodesVisible
|
|
||||||
? 'h-auto opacity-100'
|
|
||||||
: 'h-0 opacity-0',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div v-if="errors?.length" class="mt-6">
|
|
||||||
<AlertError :errors="errors" />
|
|
||||||
</div>
|
|
||||||
<div v-else class="mt-3 space-y-3">
|
|
||||||
<div
|
|
||||||
ref="recoveryCodeSectionRef"
|
|
||||||
class="bg-muted grid gap-1 rounded-lg p-4 font-mono text-sm"
|
|
||||||
>
|
|
||||||
<div v-if="!recoveryCodesList.length" class="space-y-2">
|
|
||||||
<div
|
|
||||||
v-for="n in 8"
|
|
||||||
:key="n"
|
|
||||||
class="bg-muted-foreground/20 h-4 animate-pulse rounded"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
v-for="(code, index) in recoveryCodesList"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
{{ code }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-muted-foreground text-xs select-none">
|
|
||||||
Each recovery code can be used once to access your
|
|
||||||
account and will be removed after use. If you need more,
|
|
||||||
click
|
|
||||||
<span class="font-bold">Regenerate codes</span> above.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
<p class="text-muted-foreground text-xs select-none">
|
||||||
|
Each recovery code can be used once to access your account and will
|
||||||
|
be removed after use. If you need more, click
|
||||||
|
<span class="font-bold">Regenerate codes</span> above.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import AlertError from '@/components/AlertError.vue';
|
|||||||
import InputError from '@/components/InputError.vue';
|
import InputError from '@/components/InputError.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from '@/components/ui/input-otp';
|
} from '@/components/ui/input-otp';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { useAppearance } from '@/composables/useAppearance';
|
import { useAppearance } from '@/composables/useAppearance';
|
||||||
@@ -25,8 +25,8 @@ import { confirm } from '@/routes/two-factor';
|
|||||||
import type { TwoFactorConfigContent } from '@/types';
|
import type { TwoFactorConfigContent } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
requiresConfirmation: boolean;
|
requiresConfirmation: boolean;
|
||||||
twoFactorEnabled: boolean;
|
twoFactorEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { resolvedAppearance } = useAppearance();
|
const { resolvedAppearance } = useAppearance();
|
||||||
@@ -36,7 +36,7 @@ const isOpen = defineModel<boolean>('isOpen');
|
|||||||
|
|
||||||
const { copy, copied } = useClipboard();
|
const { copy, copied } = useClipboard();
|
||||||
const { qrCodeSvg, manualSetupKey, clearSetupData, fetchSetupData, errors } =
|
const { qrCodeSvg, manualSetupKey, clearSetupData, fetchSetupData, errors } =
|
||||||
useTwoFactorAuth();
|
useTwoFactorAuth();
|
||||||
|
|
||||||
const showVerificationStep = ref(false);
|
const showVerificationStep = ref(false);
|
||||||
const code = ref<string>('');
|
const code = ref<string>('');
|
||||||
@@ -44,255 +44,234 @@ const code = ref<string>('');
|
|||||||
const pinInputContainerRef = useTemplateRef('pinInputContainerRef');
|
const pinInputContainerRef = useTemplateRef('pinInputContainerRef');
|
||||||
|
|
||||||
const modalConfig = computed<TwoFactorConfigContent>(() => {
|
const modalConfig = computed<TwoFactorConfigContent>(() => {
|
||||||
if (props.twoFactorEnabled) {
|
if (props.twoFactorEnabled) {
|
||||||
return {
|
|
||||||
title: 'Two-factor authentication enabled',
|
|
||||||
description:
|
|
||||||
'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.',
|
|
||||||
buttonText: 'Close',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showVerificationStep.value) {
|
|
||||||
return {
|
|
||||||
title: 'Verify authentication code',
|
|
||||||
description: 'Enter the 6-digit code from your authenticator app',
|
|
||||||
buttonText: 'Continue',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'Enable two-factor authentication',
|
title: 'Two-factor authentication enabled',
|
||||||
description:
|
description:
|
||||||
'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app',
|
'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.',
|
||||||
buttonText: 'Continue',
|
buttonText: 'Close',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showVerificationStep.value) {
|
||||||
|
return {
|
||||||
|
title: 'Verify authentication code',
|
||||||
|
description: 'Enter the 6-digit code from your authenticator app',
|
||||||
|
buttonText: 'Continue',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'Enable two-factor authentication',
|
||||||
|
description:
|
||||||
|
'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app',
|
||||||
|
buttonText: 'Continue',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleModalNextStep = () => {
|
const handleModalNextStep = () => {
|
||||||
if (props.requiresConfirmation) {
|
if (props.requiresConfirmation) {
|
||||||
showVerificationStep.value = true;
|
showVerificationStep.value = true;
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
pinInputContainerRef.value?.querySelector('input')?.focus();
|
pinInputContainerRef.value?.querySelector('input')?.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSetupData();
|
clearSetupData();
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetModalState = () => {
|
const resetModalState = () => {
|
||||||
if (props.twoFactorEnabled) {
|
if (props.twoFactorEnabled) {
|
||||||
clearSetupData();
|
clearSetupData();
|
||||||
}
|
}
|
||||||
|
|
||||||
showVerificationStep.value = false;
|
showVerificationStep.value = false;
|
||||||
code.value = '';
|
code.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => isOpen.value,
|
() => isOpen.value,
|
||||||
async (isOpen) => {
|
async (isOpen) => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
resetModalState();
|
resetModalState();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!qrCodeSvg.value) {
|
if (!qrCodeSvg.value) {
|
||||||
await fetchSetupData();
|
await fetchSetupData();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :open="isOpen" @update:open="isOpen = $event">
|
<Dialog :open="isOpen" @update:open="isOpen = $event">
|
||||||
<DialogContent class="sm:max-w-md">
|
<DialogContent class="sm:max-w-md">
|
||||||
<DialogHeader class="flex items-center justify-center">
|
<DialogHeader class="flex items-center justify-center">
|
||||||
<div
|
<div
|
||||||
class="border-border bg-card mb-3 w-auto rounded-full border p-0.5 shadow-sm"
|
class="border-border bg-card mb-3 w-auto rounded-full border p-0.5 shadow-sm"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border-border bg-muted relative overflow-hidden rounded-full border p-2.5"
|
class="border-border bg-muted relative overflow-hidden rounded-full border p-2.5"
|
||||||
>
|
>
|
||||||
<div
|
<div class="absolute inset-0 grid grid-cols-5 opacity-50">
|
||||||
class="absolute inset-0 grid grid-cols-5 opacity-50"
|
<div
|
||||||
>
|
v-for="i in 5"
|
||||||
<div
|
:key="`col-${i}`"
|
||||||
v-for="i in 5"
|
class="border-border border-r last:border-r-0"
|
||||||
:key="`col-${i}`"
|
/>
|
||||||
class="border-border border-r last:border-r-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 grid grid-rows-5 opacity-50"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="i in 5"
|
|
||||||
:key="`row-${i}`"
|
|
||||||
class="border-border border-b last:border-b-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ScanLine
|
|
||||||
class="text-foreground relative z-20 size-6"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogTitle>{{ modalConfig.title }}</DialogTitle>
|
|
||||||
<DialogDescription class="text-center">
|
|
||||||
{{ modalConfig.description }}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="relative flex w-auto flex-col items-center justify-center space-y-5"
|
|
||||||
>
|
|
||||||
<template v-if="!showVerificationStep">
|
|
||||||
<AlertError v-if="errors?.length" :errors="errors" />
|
|
||||||
<template v-else>
|
|
||||||
<div
|
|
||||||
class="relative mx-auto flex max-w-md items-center overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="border-border relative mx-auto aspect-square w-64 overflow-hidden rounded-lg border"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="!qrCodeSvg"
|
|
||||||
class="bg-background absolute inset-0 z-10 flex aspect-square h-auto w-full animate-pulse items-center justify-center"
|
|
||||||
>
|
|
||||||
<Spinner class="size-6" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="relative z-10 overflow-hidden border p-5"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-html="qrCodeSvg"
|
|
||||||
class="flex aspect-square size-full items-center justify-center"
|
|
||||||
:style="{
|
|
||||||
filter:
|
|
||||||
resolvedAppearance === 'dark'
|
|
||||||
? 'invert(1) brightness(1.5)'
|
|
||||||
: undefined,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex w-full items-center space-x-5">
|
|
||||||
<Button class="w-full" @click="handleModalNextStep">
|
|
||||||
{{ modalConfig.buttonText }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="relative flex w-full items-center justify-center"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-border absolute inset-0 top-1/2 h-px w-full"
|
|
||||||
/>
|
|
||||||
<span class="bg-card relative px-2 py-1"
|
|
||||||
>or, enter the code manually</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex w-full items-center justify-center space-x-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="border-border flex w-full items-stretch overflow-hidden rounded-xl border"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="!manualSetupKey"
|
|
||||||
class="bg-muted flex h-full w-full items-center justify-center p-3"
|
|
||||||
>
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
<template v-else>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
readonly
|
|
||||||
:value="manualSetupKey"
|
|
||||||
class="bg-background text-foreground h-full w-full p-3"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
@click="copy(manualSetupKey || '')"
|
|
||||||
class="border-border hover:bg-muted relative block h-auto border-l px-3"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
v-if="copied"
|
|
||||||
class="w-4 text-green-500"
|
|
||||||
/>
|
|
||||||
<Copy v-else class="w-4" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<Form
|
|
||||||
v-bind="confirm.form()"
|
|
||||||
error-bag="confirmTwoFactorAuthentication"
|
|
||||||
reset-on-error
|
|
||||||
@finish="code = ''"
|
|
||||||
@success="isOpen = false"
|
|
||||||
v-slot="{ errors, processing }"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="code" :value="code" />
|
|
||||||
<div
|
|
||||||
ref="pinInputContainerRef"
|
|
||||||
class="relative w-full space-y-3"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex w-full flex-col items-center justify-center space-y-3 py-2"
|
|
||||||
>
|
|
||||||
<InputOTP
|
|
||||||
id="otp"
|
|
||||||
v-model="code"
|
|
||||||
:maxlength="6"
|
|
||||||
:disabled="processing"
|
|
||||||
autofocus
|
|
||||||
>
|
|
||||||
<InputOTPGroup>
|
|
||||||
<InputOTPSlot
|
|
||||||
v-for="index in 6"
|
|
||||||
:key="index"
|
|
||||||
:index="index - 1"
|
|
||||||
/>
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
<InputError :message="errors?.code" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex w-full items-center space-x-5">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
class="w-auto flex-1"
|
|
||||||
@click="showVerificationStep = false"
|
|
||||||
:disabled="processing"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
class="w-auto flex-1"
|
|
||||||
:disabled="processing || code.length < 6"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
<div class="absolute inset-0 grid grid-rows-5 opacity-50">
|
||||||
</Dialog>
|
<div
|
||||||
|
v-for="i in 5"
|
||||||
|
:key="`row-${i}`"
|
||||||
|
class="border-border border-b last:border-b-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ScanLine class="text-foreground relative z-20 size-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogTitle>{{ modalConfig.title }}</DialogTitle>
|
||||||
|
<DialogDescription class="text-center">
|
||||||
|
{{ modalConfig.description }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative flex w-auto flex-col items-center justify-center space-y-5"
|
||||||
|
>
|
||||||
|
<template v-if="!showVerificationStep">
|
||||||
|
<AlertError v-if="errors?.length" :errors="errors" />
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="relative mx-auto flex max-w-md items-center overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border-border relative mx-auto aspect-square w-64 overflow-hidden rounded-lg border"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!qrCodeSvg"
|
||||||
|
class="bg-background absolute inset-0 z-10 flex aspect-square h-auto w-full animate-pulse items-center justify-center"
|
||||||
|
>
|
||||||
|
<Spinner class="size-6" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="relative z-10 overflow-hidden border p-5">
|
||||||
|
<div
|
||||||
|
v-html="qrCodeSvg"
|
||||||
|
class="flex aspect-square size-full items-center justify-center"
|
||||||
|
:style="{
|
||||||
|
filter:
|
||||||
|
resolvedAppearance === 'dark'
|
||||||
|
? 'invert(1) brightness(1.5)'
|
||||||
|
: undefined,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full items-center space-x-5">
|
||||||
|
<Button class="w-full" @click="handleModalNextStep">
|
||||||
|
{{ modalConfig.buttonText }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative flex w-full items-center justify-center">
|
||||||
|
<div class="bg-border absolute inset-0 top-1/2 h-px w-full" />
|
||||||
|
<span class="bg-card relative px-2 py-1"
|
||||||
|
>or, enter the code manually</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full items-center justify-center space-x-2">
|
||||||
|
<div
|
||||||
|
class="border-border flex w-full items-stretch overflow-hidden rounded-xl border"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!manualSetupKey"
|
||||||
|
class="bg-muted flex h-full w-full items-center justify-center p-3"
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
:value="manualSetupKey"
|
||||||
|
class="bg-background text-foreground h-full w-full p-3"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="copy(manualSetupKey || '')"
|
||||||
|
class="border-border hover:bg-muted relative block h-auto border-l px-3"
|
||||||
|
>
|
||||||
|
<Check v-if="copied" class="w-4 text-green-500" />
|
||||||
|
<Copy v-else class="w-4" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<Form
|
||||||
|
v-bind="confirm.form()"
|
||||||
|
error-bag="confirmTwoFactorAuthentication"
|
||||||
|
reset-on-error
|
||||||
|
@finish="code = ''"
|
||||||
|
@success="isOpen = false"
|
||||||
|
v-slot="{ errors, processing }"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="code" :value="code" />
|
||||||
|
<div ref="pinInputContainerRef" class="relative w-full space-y-3">
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-col items-center justify-center space-y-3 py-2"
|
||||||
|
>
|
||||||
|
<InputOTP
|
||||||
|
id="otp"
|
||||||
|
v-model="code"
|
||||||
|
:maxlength="6"
|
||||||
|
:disabled="processing"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot
|
||||||
|
v-for="index in 6"
|
||||||
|
:key="index"
|
||||||
|
:index="index - 1"
|
||||||
|
/>
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
<InputError :message="errors?.code" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full items-center space-x-5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
class="w-auto flex-1"
|
||||||
|
@click="showVerificationStep = false"
|
||||||
|
:disabled="processing"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
class="w-auto flex-1"
|
||||||
|
:disabled="processing || code.length < 6"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,34 +5,34 @@ import { useInitials } from '@/composables/useInitials';
|
|||||||
import type { User } from '@/types';
|
import type { User } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User;
|
user: User;
|
||||||
showEmail?: boolean;
|
showEmail?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
showEmail: false,
|
showEmail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getInitials } = useInitials();
|
const { getInitials } = useInitials();
|
||||||
|
|
||||||
// Compute whether we should show the avatar image
|
// Compute whether we should show the avatar image
|
||||||
const showAvatar = computed(
|
const showAvatar = computed(
|
||||||
() => props.user.avatar && props.user.avatar !== '',
|
() => props.user.avatar && props.user.avatar !== '',
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Avatar class="h-8 w-8 overflow-hidden rounded-lg">
|
<Avatar class="h-8 w-8 overflow-hidden rounded-lg">
|
||||||
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
|
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
|
||||||
<AvatarFallback class="rounded-lg text-black dark:text-white">
|
<AvatarFallback class="rounded-lg text-black dark:text-white">
|
||||||
{{ getInitials(user.name) }}
|
{{ getInitials(user.name) }}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span class="truncate font-medium">{{ user.name }}</span>
|
<span class="truncate font-medium">{{ user.name }}</span>
|
||||||
<span v-if="showEmail" class="text-muted-foreground truncate text-xs">{{
|
<span v-if="showEmail" class="text-muted-foreground truncate text-xs">{{
|
||||||
user.email
|
user.email
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { Link, router } from '@inertiajs/vue3';
|
import { Link, router } from '@inertiajs/vue3';
|
||||||
import { LogOut, Settings } from '@lucide/vue';
|
import { LogOut, Settings } from '@lucide/vue';
|
||||||
import {
|
import {
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import UserInfo from '@/components/UserInfo.vue';
|
import UserInfo from '@/components/UserInfo.vue';
|
||||||
import { logout } from '@/routes';
|
import { logout } from '@/routes';
|
||||||
@@ -13,42 +13,42 @@ import { edit } from '@/routes/profile';
|
|||||||
import type { User } from '@/types';
|
import type { User } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
router.flushAll();
|
router.flushAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DropdownMenuLabel class="p-0 font-normal">
|
<DropdownMenuLabel class="p-0 font-normal">
|
||||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<UserInfo :user="user" :show-email="true" />
|
<UserInfo :user="user" :show-email="true" />
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem :as-child="true">
|
|
||||||
<Link class="block w-full cursor-pointer" :href="edit()" prefetch>
|
|
||||||
<Settings class="mr-2 h-4 w-4" />
|
|
||||||
Settings
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem :as-child="true">
|
<DropdownMenuItem :as-child="true">
|
||||||
<Link
|
<Link class="block w-full cursor-pointer" :href="edit()" prefetch>
|
||||||
class="block w-full cursor-pointer"
|
<Settings class="mr-2 h-4 w-4" />
|
||||||
:href="logout()"
|
Settings
|
||||||
@click="handleLogout"
|
</Link>
|
||||||
as="button"
|
|
||||||
data-test="logout-button"
|
|
||||||
>
|
|
||||||
<LogOut class="mr-2 h-4 w-4" />
|
|
||||||
Log out
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem :as-child="true">
|
||||||
|
<Link
|
||||||
|
class="block w-full cursor-pointer"
|
||||||
|
:href="logout()"
|
||||||
|
@click="handleLogout"
|
||||||
|
as="button"
|
||||||
|
data-test="logout-button"
|
||||||
|
>
|
||||||
|
<LogOut class="mr-2 h-4 w-4" />
|
||||||
|
Log out
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,120 +5,115 @@ import type { Appearance, ResolvedAppearance } from '@/types';
|
|||||||
export type { Appearance, ResolvedAppearance };
|
export type { Appearance, ResolvedAppearance };
|
||||||
|
|
||||||
export type UseAppearanceReturn = {
|
export type UseAppearanceReturn = {
|
||||||
appearance: Ref<Appearance>;
|
appearance: Ref<Appearance>;
|
||||||
resolvedAppearance: ComputedRef<ResolvedAppearance>;
|
resolvedAppearance: ComputedRef<ResolvedAppearance>;
|
||||||
updateAppearance: (value: Appearance) => void;
|
updateAppearance: (value: Appearance) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function updateTheme(value: Appearance): void {
|
export function updateTheme(value: Appearance): void {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === 'system') {
|
if (value === 'system') {
|
||||||
const mediaQueryList = window.matchMedia(
|
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
'(prefers-color-scheme: dark)',
|
const systemTheme = mediaQueryList.matches ? 'dark' : 'light';
|
||||||
);
|
|
||||||
const systemTheme = mediaQueryList.matches ? 'dark' : 'light';
|
|
||||||
|
|
||||||
document.documentElement.classList.toggle(
|
document.documentElement.classList.toggle('dark', systemTheme === 'dark');
|
||||||
'dark',
|
} else {
|
||||||
systemTheme === 'dark',
|
document.documentElement.classList.toggle('dark', value === 'dark');
|
||||||
);
|
}
|
||||||
} else {
|
|
||||||
document.documentElement.classList.toggle('dark', value === 'dark');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setCookie = (name: string, value: string, days = 365) => {
|
const setCookie = (name: string, value: string, days = 365) => {
|
||||||
if (typeof document === 'undefined') {
|
if (typeof document === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxAge = days * 24 * 60 * 60;
|
const maxAge = days * 24 * 60 * 60;
|
||||||
|
|
||||||
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
|
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mediaQuery = () => {
|
const mediaQuery = () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)');
|
return window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStoredAppearance = () => {
|
const getStoredAppearance = () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return localStorage.getItem('appearance') as Appearance | null;
|
return localStorage.getItem('appearance') as Appearance | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const prefersDark = (): boolean => {
|
const prefersDark = (): boolean => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSystemThemeChange = () => {
|
const handleSystemThemeChange = () => {
|
||||||
const currentAppearance = getStoredAppearance();
|
const currentAppearance = getStoredAppearance();
|
||||||
|
|
||||||
updateTheme(currentAppearance || 'system');
|
updateTheme(currentAppearance || 'system');
|
||||||
};
|
};
|
||||||
|
|
||||||
export function initializeTheme(): void {
|
export function initializeTheme(): void {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize theme from saved preference or default to system...
|
// Initialize theme from saved preference or default to system...
|
||||||
const savedAppearance = getStoredAppearance();
|
const savedAppearance = getStoredAppearance();
|
||||||
updateTheme(savedAppearance || 'system');
|
updateTheme(savedAppearance || 'system');
|
||||||
|
|
||||||
// Set up system theme change listener...
|
// Set up system theme change listener...
|
||||||
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
|
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appearance = ref<Appearance>('system');
|
const appearance = ref<Appearance>('system');
|
||||||
|
|
||||||
export function useAppearance(): UseAppearanceReturn {
|
export function useAppearance(): UseAppearanceReturn {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const savedAppearance = localStorage.getItem(
|
const savedAppearance = localStorage.getItem(
|
||||||
'appearance',
|
'appearance',
|
||||||
) as Appearance | null;
|
) as Appearance | null;
|
||||||
|
|
||||||
if (savedAppearance) {
|
if (savedAppearance) {
|
||||||
appearance.value = savedAppearance;
|
appearance.value = savedAppearance;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedAppearance = computed<ResolvedAppearance>(() => {
|
const resolvedAppearance = computed<ResolvedAppearance>(() => {
|
||||||
if (appearance.value === 'system') {
|
if (appearance.value === 'system') {
|
||||||
return prefersDark() ? 'dark' : 'light';
|
return prefersDark() ? 'dark' : 'light';
|
||||||
}
|
|
||||||
|
|
||||||
return appearance.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateAppearance(value: Appearance) {
|
|
||||||
appearance.value = value;
|
|
||||||
|
|
||||||
// Store in localStorage for client-side persistence...
|
|
||||||
localStorage.setItem('appearance', value);
|
|
||||||
|
|
||||||
// Store in cookie for SSR...
|
|
||||||
setCookie('appearance', value);
|
|
||||||
|
|
||||||
updateTheme(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return appearance.value;
|
||||||
appearance,
|
});
|
||||||
resolvedAppearance,
|
|
||||||
updateAppearance,
|
function updateAppearance(value: Appearance) {
|
||||||
};
|
appearance.value = value;
|
||||||
|
|
||||||
|
// Store in localStorage for client-side persistence...
|
||||||
|
localStorage.setItem('appearance', value);
|
||||||
|
|
||||||
|
// Store in cookie for SSR...
|
||||||
|
setCookie('appearance', value);
|
||||||
|
|
||||||
|
updateTheme(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appearance,
|
||||||
|
resolvedAppearance,
|
||||||
|
updateAppearance,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,78 +5,78 @@ import { computed, readonly } from 'vue';
|
|||||||
import { toUrl } from '@/lib/utils';
|
import { toUrl } from '@/lib/utils';
|
||||||
|
|
||||||
export type UseCurrentUrlReturn = {
|
export type UseCurrentUrlReturn = {
|
||||||
currentUrl: DeepReadonly<ComputedRef<string>>;
|
currentUrl: DeepReadonly<ComputedRef<string>>;
|
||||||
isCurrentUrl: (
|
isCurrentUrl: (
|
||||||
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
||||||
currentUrl?: string,
|
currentUrl?: string,
|
||||||
startsWith?: boolean,
|
startsWith?: boolean,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
isCurrentOrParentUrl: (
|
isCurrentOrParentUrl: (
|
||||||
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
||||||
currentUrl?: string,
|
currentUrl?: string,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
whenCurrentUrl: <T, F = null>(
|
whenCurrentUrl: <T, F = null>(
|
||||||
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
||||||
ifTrue: T,
|
ifTrue: T,
|
||||||
ifFalse?: F,
|
ifFalse?: F,
|
||||||
) => T | F;
|
) => T | F;
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = usePage();
|
const page = usePage();
|
||||||
const currentUrlReactive = computed(
|
const currentUrlReactive = computed(
|
||||||
() =>
|
() =>
|
||||||
new URL(
|
new URL(
|
||||||
page.url,
|
page.url,
|
||||||
typeof window !== 'undefined'
|
typeof window !== 'undefined'
|
||||||
? window.location.origin
|
? window.location.origin
|
||||||
: 'http://localhost',
|
: 'http://localhost',
|
||||||
).pathname,
|
).pathname,
|
||||||
);
|
);
|
||||||
|
|
||||||
export function useCurrentUrl(): UseCurrentUrlReturn {
|
export function useCurrentUrl(): UseCurrentUrlReturn {
|
||||||
function isCurrentUrl(
|
function isCurrentUrl(
|
||||||
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
||||||
currentUrl?: string,
|
currentUrl?: string,
|
||||||
startsWith: boolean = false,
|
startsWith: boolean = false,
|
||||||
) {
|
) {
|
||||||
const urlToCompare = currentUrl ?? currentUrlReactive.value;
|
const urlToCompare = currentUrl ?? currentUrlReactive.value;
|
||||||
const urlString = toUrl(urlToCheck);
|
const urlString = toUrl(urlToCheck);
|
||||||
|
|
||||||
const comparePath = (path: string): boolean =>
|
const comparePath = (path: string): boolean =>
|
||||||
startsWith ? urlToCompare.startsWith(path) : path === urlToCompare;
|
startsWith ? urlToCompare.startsWith(path) : path === urlToCompare;
|
||||||
|
|
||||||
if (!urlString.startsWith('http')) {
|
if (!urlString.startsWith('http')) {
|
||||||
return comparePath(urlString);
|
return comparePath(urlString);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const absoluteUrl = new URL(urlString);
|
|
||||||
|
|
||||||
return comparePath(absoluteUrl.pathname);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCurrentOrParentUrl(
|
try {
|
||||||
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
const absoluteUrl = new URL(urlString);
|
||||||
currentUrl?: string,
|
|
||||||
) {
|
|
||||||
return isCurrentUrl(urlToCheck, currentUrl, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function whenCurrentUrl(
|
return comparePath(absoluteUrl.pathname);
|
||||||
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
} catch {
|
||||||
ifTrue: any,
|
return false;
|
||||||
ifFalse: any = null,
|
|
||||||
) {
|
|
||||||
return isCurrentUrl(urlToCheck) ? ifTrue : ifFalse;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
function isCurrentOrParentUrl(
|
||||||
currentUrl: readonly(currentUrlReactive),
|
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
||||||
isCurrentUrl,
|
currentUrl?: string,
|
||||||
isCurrentOrParentUrl,
|
) {
|
||||||
whenCurrentUrl,
|
return isCurrentUrl(urlToCheck, currentUrl, true);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function whenCurrentUrl(
|
||||||
|
urlToCheck: NonNullable<InertiaLinkProps['href']>,
|
||||||
|
ifTrue: any,
|
||||||
|
ifFalse: any = null,
|
||||||
|
) {
|
||||||
|
return isCurrentUrl(urlToCheck) ? ifTrue : ifFalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentUrl: readonly(currentUrlReactive),
|
||||||
|
isCurrentUrl,
|
||||||
|
isCurrentOrParentUrl,
|
||||||
|
whenCurrentUrl,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
export type UseInitialsReturn = {
|
export type UseInitialsReturn = {
|
||||||
getInitials: (fullName?: string) => string;
|
getInitials: (fullName?: string) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getInitial(name: string): string {
|
function getInitial(name: string): string {
|
||||||
return Array.from(name)[0] ?? '';
|
return Array.from(name)[0] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInitials(fullName?: string): string {
|
export function getInitials(fullName?: string): string {
|
||||||
if (!fullName) {
|
if (!fullName) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const names = fullName.trim().split(/\s+/u).filter(Boolean);
|
const names = fullName.trim().split(/\s+/u).filter(Boolean);
|
||||||
|
|
||||||
if (names.length === 0) {
|
if (names.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (names.length === 1) {
|
if (names.length === 1) {
|
||||||
return getInitial(names[0]).toUpperCase();
|
return getInitial(names[0]).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${getInitial(names[0])}${getInitial(names[names.length - 1])}`.toUpperCase();
|
return `${getInitial(names[0])}${getInitial(names[names.length - 1])}`.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInitials(): UseInitialsReturn {
|
export function useInitials(): UseInitialsReturn {
|
||||||
return { getInitials };
|
return { getInitials };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import { computed, ref } from 'vue';
|
|||||||
import { qrCode, recoveryCodes, secretKey } from '@/routes/two-factor';
|
import { qrCode, recoveryCodes, secretKey } from '@/routes/two-factor';
|
||||||
|
|
||||||
export type UseTwoFactorAuthReturn = {
|
export type UseTwoFactorAuthReturn = {
|
||||||
qrCodeSvg: Ref<string | null>;
|
qrCodeSvg: Ref<string | null>;
|
||||||
manualSetupKey: Ref<string | null>;
|
manualSetupKey: Ref<string | null>;
|
||||||
recoveryCodesList: Ref<string[]>;
|
recoveryCodesList: Ref<string[]>;
|
||||||
errors: Ref<string[]>;
|
errors: Ref<string[]>;
|
||||||
hasSetupData: ComputedRef<boolean>;
|
hasSetupData: ComputedRef<boolean>;
|
||||||
clearSetupData: () => void;
|
clearSetupData: () => void;
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
clearTwoFactorAuthData: () => void;
|
clearTwoFactorAuthData: () => void;
|
||||||
fetchQrCode: () => Promise<void>;
|
fetchQrCode: () => Promise<void>;
|
||||||
fetchSetupKey: () => Promise<void>;
|
fetchSetupKey: () => Promise<void>;
|
||||||
fetchSetupData: () => Promise<void>;
|
fetchSetupData: () => Promise<void>;
|
||||||
fetchRecoveryCodes: () => Promise<void>;
|
fetchRecoveryCodes: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = ref<string[]>([]);
|
const errors = ref<string[]>([]);
|
||||||
@@ -24,89 +24,89 @@ const qrCodeSvg = ref<string | null>(null);
|
|||||||
const recoveryCodesList = ref<string[]>([]);
|
const recoveryCodesList = ref<string[]>([]);
|
||||||
|
|
||||||
const hasSetupData = computed<boolean>(
|
const hasSetupData = computed<boolean>(
|
||||||
() => qrCodeSvg.value !== null && manualSetupKey.value !== null,
|
() => qrCodeSvg.value !== null && manualSetupKey.value !== null,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
|
export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
|
||||||
const http = useHttp();
|
const http = useHttp();
|
||||||
|
|
||||||
const fetchQrCode = async (): Promise<void> => {
|
const fetchQrCode = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { svg } = (await http.submit(qrCode())) as {
|
const { svg } = (await http.submit(qrCode())) as {
|
||||||
svg: string;
|
svg: string;
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
qrCodeSvg.value = svg;
|
qrCodeSvg.value = svg;
|
||||||
} catch {
|
} catch {
|
||||||
errors.value.push('Failed to fetch QR code');
|
errors.value.push('Failed to fetch QR code');
|
||||||
qrCodeSvg.value = null;
|
qrCodeSvg.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSetupKey = async (): Promise<void> => {
|
const fetchSetupKey = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { secretKey: key } = (await http.submit(secretKey())) as {
|
const { secretKey: key } = (await http.submit(secretKey())) as {
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
manualSetupKey.value = key;
|
manualSetupKey.value = key;
|
||||||
} catch {
|
} catch {
|
||||||
errors.value.push('Failed to fetch a setup key');
|
errors.value.push('Failed to fetch a setup key');
|
||||||
manualSetupKey.value = null;
|
manualSetupKey.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearSetupData = (): void => {
|
const clearSetupData = (): void => {
|
||||||
manualSetupKey.value = null;
|
manualSetupKey.value = null;
|
||||||
qrCodeSvg.value = null;
|
qrCodeSvg.value = null;
|
||||||
clearErrors();
|
clearErrors();
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearErrors = (): void => {
|
const clearErrors = (): void => {
|
||||||
errors.value = [];
|
errors.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearTwoFactorAuthData = (): void => {
|
const clearTwoFactorAuthData = (): void => {
|
||||||
clearSetupData();
|
clearSetupData();
|
||||||
clearErrors();
|
clearErrors();
|
||||||
recoveryCodesList.value = [];
|
recoveryCodesList.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchRecoveryCodes = async (): Promise<void> => {
|
const fetchRecoveryCodes = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
clearErrors();
|
clearErrors();
|
||||||
recoveryCodesList.value = (await http.submit(
|
recoveryCodesList.value = (await http.submit(
|
||||||
recoveryCodes(),
|
recoveryCodes(),
|
||||||
)) as string[];
|
)) as string[];
|
||||||
} catch {
|
} catch {
|
||||||
errors.value.push('Failed to fetch recovery codes');
|
errors.value.push('Failed to fetch recovery codes');
|
||||||
recoveryCodesList.value = [];
|
recoveryCodesList.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSetupData = async (): Promise<void> => {
|
const fetchSetupData = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
clearErrors();
|
clearErrors();
|
||||||
await Promise.all([fetchQrCode(), fetchSetupKey()]);
|
await Promise.all([fetchQrCode(), fetchSetupKey()]);
|
||||||
} catch {
|
} catch {
|
||||||
qrCodeSvg.value = null;
|
qrCodeSvg.value = null;
|
||||||
manualSetupKey.value = null;
|
manualSetupKey.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
qrCodeSvg,
|
qrCodeSvg,
|
||||||
manualSetupKey,
|
manualSetupKey,
|
||||||
recoveryCodesList,
|
recoveryCodesList,
|
||||||
errors,
|
errors,
|
||||||
hasSetupData,
|
hasSetupData,
|
||||||
clearSetupData,
|
clearSetupData,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
clearTwoFactorAuthData,
|
clearTwoFactorAuthData,
|
||||||
fetchQrCode,
|
fetchQrCode,
|
||||||
fetchSetupKey,
|
fetchSetupKey,
|
||||||
fetchSetupData,
|
fetchSetupData,
|
||||||
fetchRecoveryCodes,
|
fetchRecoveryCodes,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
|
|||||||
import type { BreadcrumbItem } from '@/types';
|
import type { BreadcrumbItem } from '@/types';
|
||||||
|
|
||||||
const { breadcrumbs = [] } = defineProps<{
|
const { breadcrumbs = [] } = defineProps<{
|
||||||
breadcrumbs?: BreadcrumbItem[];
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout :breadcrumbs="breadcrumbs">
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||||||
<slot />
|
<slot />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
import AuthLayout from '@/layouts/auth/AuthSimpleLayout.vue';
|
import AuthLayout from '@/layouts/auth/AuthSimpleLayout.vue';
|
||||||
|
|
||||||
const { title = '', description = '' } = defineProps<{
|
const { title = '', description = '' } = defineProps<{
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthLayout :title="title" :description="description">
|
<AuthLayout :title="title" :description="description">
|
||||||
<slot />
|
<slot />
|
||||||
</AuthLayout>
|
</AuthLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,20 +6,20 @@ import { Toaster } from '@/components/ui/sonner';
|
|||||||
import type { BreadcrumbItem } from '@/types';
|
import type { BreadcrumbItem } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
breadcrumbs?: BreadcrumbItem[];
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
breadcrumbs: () => [],
|
breadcrumbs: () => [],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppShell variant="header">
|
<AppShell variant="header">
|
||||||
<AppHeader :breadcrumbs="breadcrumbs" />
|
<AppHeader :breadcrumbs="breadcrumbs" />
|
||||||
<AppContent variant="header">
|
<AppContent variant="header">
|
||||||
<slot />
|
<slot />
|
||||||
</AppContent>
|
</AppContent>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ import { Toaster } from '@/components/ui/sonner';
|
|||||||
import type { BreadcrumbItem } from '@/types';
|
import type { BreadcrumbItem } from '@/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
breadcrumbs?: BreadcrumbItem[];
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
breadcrumbs: () => [],
|
breadcrumbs: () => [],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppShell variant="sidebar">
|
<AppShell variant="sidebar">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<AppContent variant="sidebar" class="min-w-0 overflow-x-clip">
|
<AppContent variant="sidebar" class="min-w-0 overflow-x-clip">
|
||||||
<AppSidebarHeader :breadcrumbs="breadcrumbs" />
|
<AppSidebarHeader :breadcrumbs="breadcrumbs" />
|
||||||
<slot />
|
<slot />
|
||||||
</AppContent>
|
</AppContent>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,49 +2,47 @@
|
|||||||
import { Link } from '@inertiajs/vue3';
|
import { Link } from '@inertiajs/vue3';
|
||||||
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { home } from '@/routes';
|
import { home } from '@/routes';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"
|
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">
|
<div class="flex w-full max-w-md flex-col gap-6">
|
||||||
<Link
|
<Link
|
||||||
:href="home()"
|
:href="home()"
|
||||||
class="flex items-center gap-2 self-center font-medium"
|
class="flex items-center gap-2 self-center font-medium"
|
||||||
>
|
>
|
||||||
<div class="flex h-9 w-9 items-center justify-center">
|
<div class="flex h-9 w-9 items-center justify-center">
|
||||||
<AppLogoIcon
|
<AppLogoIcon class="size-9 fill-current text-black dark:text-white" />
|
||||||
class="size-9 fill-current text-black dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-6">
|
|
||||||
<Card class="rounded-xl">
|
|
||||||
<CardHeader class="px-10 pt-8 pb-0 text-center">
|
|
||||||
<CardTitle class="text-xl">{{ title }}</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{{ description }}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="px-10 py-8">
|
|
||||||
<slot />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<Card class="rounded-xl">
|
||||||
|
<CardHeader class="px-10 pt-8 pb-0 text-center">
|
||||||
|
<CardTitle class="text-xl">{{ title }}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{{ description }}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="px-10 py-8">
|
||||||
|
<slot />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,40 +4,40 @@ import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
|||||||
import { home } from '@/routes';
|
import { home } from '@/routes';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"
|
class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"
|
||||||
>
|
>
|
||||||
<div class="w-full max-w-sm">
|
<div class="w-full max-w-sm">
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<Link
|
<Link
|
||||||
:href="home()"
|
:href="home()"
|
||||||
class="flex flex-col items-center gap-2 font-medium"
|
class="flex flex-col items-center gap-2 font-medium"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mb-1 flex h-9 w-9 items-center justify-center rounded-md"
|
class="mb-1 flex h-9 w-9 items-center justify-center rounded-md"
|
||||||
>
|
>
|
||||||
<AppLogoIcon
|
<AppLogoIcon
|
||||||
class="size-9 fill-current text-[var(--foreground)] dark:text-white"
|
class="size-9 fill-current text-[var(--foreground)] dark:text-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<span class="sr-only">{{ title }}</span>
|
|
||||||
</Link>
|
|
||||||
<div class="space-y-2 text-center">
|
|
||||||
<h1 class="text-xl font-medium">{{ title }}</h1>
|
|
||||||
<p class="text-muted-foreground text-center text-sm">
|
|
||||||
{{ description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<slot />
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="sr-only">{{ title }}</span>
|
||||||
|
</Link>
|
||||||
|
<div class="space-y-2 text-center">
|
||||||
|
<h1 class="text-xl font-medium">{{ title }}</h1>
|
||||||
|
<p class="text-muted-foreground text-center text-sm">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,41 +7,41 @@ const page = usePage();
|
|||||||
const name = page.props.name;
|
const name = page.props.name;
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<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
|
<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"
|
class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-r"
|
||||||
>
|
>
|
||||||
<div
|
<div class="absolute inset-0 bg-zinc-900" />
|
||||||
class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-r"
|
<Link
|
||||||
>
|
:href="home()"
|
||||||
<div class="absolute inset-0 bg-zinc-900" />
|
class="relative z-20 flex items-center text-lg font-medium"
|
||||||
<Link
|
>
|
||||||
:href="home()"
|
<AppLogoIcon class="mr-2 size-8 fill-current text-white" />
|
||||||
class="relative z-20 flex items-center text-lg font-medium"
|
{{ name }}
|
||||||
>
|
</Link>
|
||||||
<AppLogoIcon class="mr-2 size-8 fill-current text-white" />
|
|
||||||
{{ name }}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div class="lg:p-8">
|
|
||||||
<div
|
|
||||||
class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col space-y-2 text-center">
|
|
||||||
<h1 class="text-xl font-medium tracking-tight" v-if="title">
|
|
||||||
{{ title }}
|
|
||||||
</h1>
|
|
||||||
<p class="text-muted-foreground text-sm" v-if="description">
|
|
||||||
{{ description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="lg:p-8">
|
||||||
|
<div
|
||||||
|
class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col space-y-2 text-center">
|
||||||
|
<h1 class="text-xl font-medium tracking-tight" v-if="title">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted-foreground text-sm" v-if="description">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,61 +11,58 @@ import { edit as editSecurity } from '@/routes/security';
|
|||||||
import type { NavItem } from '@/types';
|
import type { NavItem } from '@/types';
|
||||||
|
|
||||||
const sidebarNavItems: NavItem[] = [
|
const sidebarNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Profile',
|
title: 'Profile',
|
||||||
href: editProfile(),
|
href: editProfile(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Security',
|
title: 'Security',
|
||||||
href: editSecurity(),
|
href: editSecurity(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Appearance',
|
title: 'Appearance',
|
||||||
href: editAppearance(),
|
href: editAppearance(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const { isCurrentOrParentUrl } = useCurrentUrl();
|
const { isCurrentOrParentUrl } = useCurrentUrl();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-4 py-6">
|
<div class="px-4 py-6">
|
||||||
<Heading
|
<Heading
|
||||||
title="Settings"
|
title="Settings"
|
||||||
description="Manage your profile and account settings"
|
description="Manage your profile and account settings"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col lg:flex-row lg:space-x-12">
|
<div class="flex flex-col lg:flex-row lg:space-x-12">
|
||||||
<aside class="w-full max-w-xl lg:w-48">
|
<aside class="w-full max-w-xl lg:w-48">
|
||||||
<nav
|
<nav class="flex flex-col space-y-1 space-x-0" aria-label="Settings">
|
||||||
class="flex flex-col space-y-1 space-x-0"
|
<Button
|
||||||
aria-label="Settings"
|
v-for="item in sidebarNavItems"
|
||||||
>
|
:key="toUrl(item.href)"
|
||||||
<Button
|
variant="ghost"
|
||||||
v-for="item in sidebarNavItems"
|
:class="[
|
||||||
:key="toUrl(item.href)"
|
'w-full justify-start',
|
||||||
variant="ghost"
|
{ 'bg-muted': isCurrentOrParentUrl(item.href) },
|
||||||
:class="[
|
]"
|
||||||
'w-full justify-start',
|
as-child
|
||||||
{ 'bg-muted': isCurrentOrParentUrl(item.href) },
|
>
|
||||||
]"
|
<Link :href="item.href">
|
||||||
as-child
|
<component :is="item.icon" class="h-4 w-4" />
|
||||||
>
|
{{ item.title }}
|
||||||
<Link :href="item.href">
|
</Link>
|
||||||
<component :is="item.icon" class="h-4 w-4" />
|
</Button>
|
||||||
{{ item.title }}
|
</nav>
|
||||||
</Link>
|
</aside>
|
||||||
</Button>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<Separator class="my-6 lg:hidden" />
|
<Separator class="my-6 lg:hidden" />
|
||||||
|
|
||||||
<div class="flex-1 md:max-w-2xl">
|
<div class="flex-1 md:max-w-2xl">
|
||||||
<section class="max-w-xl space-y-12">
|
<section class="max-w-xl space-y-12">
|
||||||
<slot />
|
<slot />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { toast } from 'vue-sonner';
|
|||||||
import type { FlashToast } from '@/types/ui';
|
import type { FlashToast } from '@/types/ui';
|
||||||
|
|
||||||
export function initializeFlashToast(): void {
|
export function initializeFlashToast(): void {
|
||||||
router.on('flash', (event) => {
|
router.on('flash', (event) => {
|
||||||
const flash = (event as CustomEvent).detail?.flash;
|
const flash = (event as CustomEvent).detail?.flash;
|
||||||
const data = flash?.toast as FlashToast | undefined;
|
const data = flash?.toast as FlashToast | undefined;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast[data.type](data.message);
|
toast[data.type](data.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { InertiaLinkProps } from '@inertiajs/vue3';
|
import type { InertiaLinkProps } from '@inertiajs/vue3';
|
||||||
import { clsx } from 'clsx';
|
|
||||||
import type { ClassValue } from 'clsx';
|
import type { ClassValue } from 'clsx';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toUrl(href: NonNullable<InertiaLinkProps['href']>) {
|
export function toUrl(href: NonNullable<InertiaLinkProps['href']>) {
|
||||||
return typeof href === 'string' ? href : href?.url;
|
return typeof href === 'string' ? href : href?.url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,44 +4,42 @@ import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
|
|||||||
import { dashboard } from '@/routes';
|
import { dashboard } from '@/routes';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
href: dashboard(),
|
href: dashboard(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Dashboard" />
|
<Head title="Dashboard" />
|
||||||
|
|
||||||
<div
|
<div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
|
||||||
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4"
|
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||||
>
|
<div
|
||||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
class="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border"
|
||||||
<div
|
>
|
||||||
class="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border"
|
<PlaceholderPattern />
|
||||||
>
|
</div>
|
||||||
<PlaceholderPattern />
|
<div
|
||||||
</div>
|
class="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border"
|
||||||
<div
|
>
|
||||||
class="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border"
|
<PlaceholderPattern />
|
||||||
>
|
</div>
|
||||||
<PlaceholderPattern />
|
<div
|
||||||
</div>
|
class="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border"
|
||||||
<div
|
>
|
||||||
class="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border"
|
<PlaceholderPattern />
|
||||||
>
|
</div>
|
||||||
<PlaceholderPattern />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="border-sidebar-border/70 dark:border-sidebar-border relative min-h-[100vh] flex-1 rounded-xl border md:min-h-min"
|
|
||||||
>
|
|
||||||
<PlaceholderPattern />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="border-sidebar-border/70 dark:border-sidebar-border relative min-h-[100vh] flex-1 rounded-xl border md:min-h-min"
|
||||||
|
>
|
||||||
|
<PlaceholderPattern />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+411
-426
@@ -5,439 +5,424 @@ import { register } from '@/routes';
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Welcome">
|
<Head title="Welcome">
|
||||||
<link rel="preconnect" href="https://rsms.me/" />
|
<link rel="preconnect" href="https://rsms.me/" />
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||||
</Head>
|
</Head>
|
||||||
<div
|
<div
|
||||||
class="flex min-h-screen flex-col items-center bg-[#FDFDFC] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]"
|
class="flex min-h-screen flex-col items-center bg-[#FDFDFC] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="mb-6 w-full max-w-[335px] text-sm not-has-[nav]:hidden lg:max-w-4xl"
|
||||||
>
|
>
|
||||||
<header
|
<nav class="flex items-center justify-end gap-4">
|
||||||
class="mb-6 w-full max-w-[335px] text-sm not-has-[nav]:hidden lg:max-w-4xl"
|
<Link
|
||||||
|
v-if="$page.props.auth.user"
|
||||||
|
:href="dashboard()"
|
||||||
|
class="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
|
||||||
>
|
>
|
||||||
<nav class="flex items-center justify-end gap-4">
|
Dashboard
|
||||||
<Link
|
</Link>
|
||||||
v-if="$page.props.auth.user"
|
<template v-else>
|
||||||
:href="dashboard()"
|
<Link
|
||||||
class="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
|
:href="login()"
|
||||||
>
|
class="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]"
|
||||||
Dashboard
|
>
|
||||||
</Link>
|
Log in
|
||||||
<template v-else>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
:href="login()"
|
:href="register()"
|
||||||
class="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]"
|
class="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
|
||||||
>
|
>
|
||||||
Log in
|
Register
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
</template>
|
||||||
:href="register()"
|
</nav>
|
||||||
class="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
|
</header>
|
||||||
>
|
<div
|
||||||
Register
|
class="flex w-full items-center justify-center opacity-100 transition-opacity duration-750 lg:grow starting:opacity-0"
|
||||||
</Link>
|
>
|
||||||
</template>
|
<main
|
||||||
</nav>
|
class="flex w-full max-w-[335px] flex-col-reverse overflow-hidden rounded-lg lg:max-w-4xl lg:flex-row"
|
||||||
</header>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex w-full items-center justify-center opacity-100 transition-opacity duration-750 lg:grow starting:opacity-0"
|
class="flex-1 rounded-br-lg rounded-bl-lg bg-white p-6 pb-12 text-[13px] leading-[20px] shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] lg:rounded-tl-lg lg:rounded-br-none lg:p-20 dark:bg-[#161615] dark:text-[#EDEDEC] dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d]"
|
||||||
>
|
>
|
||||||
<main
|
<h1 class="mb-1 font-medium">Let's get started</h1>
|
||||||
class="flex w-full max-w-[335px] flex-col-reverse overflow-hidden rounded-lg lg:max-w-4xl lg:flex-row"
|
<p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]">
|
||||||
|
Laravel has an incredibly rich ecosystem. <br />We suggest starting
|
||||||
|
with the following.
|
||||||
|
</p>
|
||||||
|
<ul class="mb-4 flex flex-col lg:mb-6">
|
||||||
|
<li
|
||||||
|
class="relative flex items-center gap-4 py-2 before:absolute before:top-1/2 before:bottom-0 before:left-[0.4rem] before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A]"
|
||||||
>
|
>
|
||||||
<div
|
<span class="relative bg-white py-1 dark:bg-[#161615]">
|
||||||
class="flex-1 rounded-br-lg rounded-bl-lg bg-white p-6 pb-12 text-[13px] leading-[20px] shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] lg:rounded-tl-lg lg:rounded-br-none lg:p-20 dark:bg-[#161615] dark:text-[#EDEDEC] dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d]"
|
<span
|
||||||
|
class="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#e3e3e0] bg-[#FDFDFC] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] dark:border-[#3E3E3A] dark:bg-[#161615]"
|
||||||
>
|
>
|
||||||
<h1 class="mb-1 font-medium">Let's get started</h1>
|
<span
|
||||||
<p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]">
|
class="h-1.5 w-1.5 rounded-full bg-[#dbdbd7] dark:bg-[#3E3E3A]"
|
||||||
Laravel has an incredibly rich ecosystem. <br />We
|
/>
|
||||||
suggest starting with the following.
|
</span>
|
||||||
</p>
|
</span>
|
||||||
<ul class="mb-4 flex flex-col lg:mb-6">
|
<span>
|
||||||
<li
|
Read the
|
||||||
class="relative flex items-center gap-4 py-2 before:absolute before:top-1/2 before:bottom-0 before:left-[0.4rem] before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A]"
|
<a
|
||||||
>
|
href="https://laravel.com/docs"
|
||||||
<span
|
target="_blank"
|
||||||
class="relative bg-white py-1 dark:bg-[#161615]"
|
class="ml-1 inline-flex items-center space-x-1 font-medium text-[#f53003] underline underline-offset-4 dark:text-[#FF4433]"
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#e3e3e0] bg-[#FDFDFC] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] dark:border-[#3E3E3A] dark:bg-[#161615]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="h-1.5 w-1.5 rounded-full bg-[#dbdbd7] dark:bg-[#3E3E3A]"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Read the
|
|
||||||
<a
|
|
||||||
href="https://laravel.com/docs"
|
|
||||||
target="_blank"
|
|
||||||
class="ml-1 inline-flex items-center space-x-1 font-medium text-[#f53003] underline underline-offset-4 dark:text-[#FF4433]"
|
|
||||||
>
|
|
||||||
<span>Documentation</span>
|
|
||||||
<svg
|
|
||||||
width="10"
|
|
||||||
height="11"
|
|
||||||
viewBox="0 0 10 11"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-2.5 w-2.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="square"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="relative flex items-center gap-4 py-2 before:absolute before:top-0 before:bottom-1/2 before:left-[0.4rem] before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="relative bg-white py-1 dark:bg-[#161615]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#e3e3e0] bg-[#FDFDFC] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] dark:border-[#3E3E3A] dark:bg-[#161615]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="h-1.5 w-1.5 rounded-full bg-[#dbdbd7] dark:bg-[#3E3E3A]"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Watch video tutorials at
|
|
||||||
<a
|
|
||||||
href="https://laracasts.com"
|
|
||||||
target="_blank"
|
|
||||||
class="ml-1 inline-flex items-center space-x-1 font-medium text-[#f53003] underline underline-offset-4 dark:text-[#FF4433]"
|
|
||||||
>
|
|
||||||
<span>Laracasts</span>
|
|
||||||
<svg
|
|
||||||
width="10"
|
|
||||||
height="11"
|
|
||||||
viewBox="0 0 10 11"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-2.5 w-2.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="square"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="flex gap-3 text-sm leading-normal">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://cloud.laravel.com"
|
|
||||||
target="_blank"
|
|
||||||
class="inline-block rounded-sm border border-black bg-[#1b1b18] px-5 py-1.5 text-sm leading-normal text-white hover:border-black hover:bg-black dark:border-[#eeeeec] dark:bg-[#eeeeec] dark:text-[#1C1C1A] dark:hover:border-white dark:hover:bg-white"
|
|
||||||
>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative -mb-px aspect-[335/364] w-full shrink-0 overflow-hidden rounded-t-lg bg-[#fff2f2] lg:mb-0 lg:-ml-px lg:aspect-auto lg:w-[438px] lg:rounded-t-none lg:rounded-r-lg dark:bg-[#1D0002]"
|
|
||||||
>
|
>
|
||||||
<!-- Laravel Logo -->
|
<span>Documentation</span>
|
||||||
<svg
|
<svg
|
||||||
class="w-full max-w-none translate-y-0 text-[#F53003] opacity-100 transition-all duration-750 dark:text-[#F61500] starting:opacity-0 motion-safe:starting:translate-y-6"
|
width="10"
|
||||||
viewBox="0 0 438 104"
|
height="11"
|
||||||
fill="none"
|
viewBox="0 0 10 11"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
fill="none"
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path
|
class="h-2.5 w-2.5"
|
||||||
d="M17.2036 -3H0V102.197H49.5189V86.7187H17.2036V-3Z"
|
>
|
||||||
fill="currentColor"
|
<path
|
||||||
/>
|
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
|
||||||
<path
|
stroke="currentColor"
|
||||||
d="M110.256 41.6337C108.061 38.1275 104.945 35.3731 100.905 33.3681C96.8667 31.3647 92.8016 30.3618 88.7131 30.3618C83.4247 30.3618 78.5885 31.3389 74.201 33.2923C69.8111 35.2456 66.0474 37.928 62.9059 41.3333C59.7643 44.7401 57.3198 48.6726 55.5754 53.1293C53.8287 57.589 52.9572 62.274 52.9572 67.1813C52.9572 72.1925 53.8287 76.8995 55.5754 81.3069C57.3191 85.7173 59.7636 89.6241 62.9059 93.0293C66.0474 96.4361 69.8119 99.1155 74.201 101.069C78.5885 103.022 83.4247 103.999 88.7131 103.999C92.8016 103.999 96.8667 102.997 100.905 100.994C104.945 98.9911 108.061 96.2359 110.256 92.7282V102.195H126.563V32.1642H110.256V41.6337ZM108.76 75.7472C107.762 78.4531 106.366 80.8078 104.572 82.8112C102.776 84.8161 100.606 86.4183 98.0637 87.6206C95.5202 88.823 92.7004 89.4238 89.6103 89.4238C86.5178 89.4238 83.7252 88.823 81.2324 87.6206C78.7388 86.4183 76.5949 84.8161 74.7998 82.8112C73.004 80.8078 71.6319 78.4531 70.6856 75.7472C69.7356 73.0421 69.2644 70.1868 69.2644 67.1821C69.2644 64.1758 69.7356 61.3205 70.6856 58.6154C71.6319 55.9102 73.004 53.5571 74.7998 51.5522C76.5949 49.5495 78.738 47.9451 81.2324 46.7427C83.7252 45.5404 86.5178 44.9396 89.6103 44.9396C92.7012 44.9396 95.5202 45.5404 98.0637 46.7427C100.606 47.9451 102.776 49.5487 104.572 51.5522C106.367 53.5571 107.762 55.9102 108.76 58.6154C109.756 61.3205 110.256 64.1758 110.256 67.1821C110.256 70.1868 109.756 73.0421 108.76 75.7472Z"
|
stroke-linecap="square"
|
||||||
fill="currentColor"
|
/>
|
||||||
/>
|
</svg>
|
||||||
<path
|
</a>
|
||||||
d="M242.805 41.6337C240.611 38.1275 237.494 35.3731 233.455 33.3681C229.416 31.3647 225.351 30.3618 221.262 30.3618C215.974 30.3618 211.138 31.3389 206.75 33.2923C202.36 35.2456 198.597 37.928 195.455 41.3333C192.314 44.7401 189.869 48.6726 188.125 53.1293C186.378 57.589 185.507 62.274 185.507 67.1813C185.507 72.1925 186.378 76.8995 188.125 81.3069C189.868 85.7173 192.313 89.6241 195.455 93.0293C198.597 96.4361 202.361 99.1155 206.75 101.069C211.138 103.022 215.974 103.999 221.262 103.999C225.351 103.999 229.416 102.997 233.455 100.994C237.494 98.9911 240.611 96.2359 242.805 92.7282V102.195H259.112V32.1642H242.805V41.6337ZM241.31 75.7472C240.312 78.4531 238.916 80.8078 237.122 82.8112C235.326 84.8161 233.156 86.4183 230.614 87.6206C228.07 88.823 225.251 89.4238 222.16 89.4238C219.068 89.4238 216.275 88.823 213.782 87.6206C211.289 86.4183 209.145 84.8161 207.35 82.8112C205.554 80.8078 204.182 78.4531 203.236 75.7472C202.286 73.0421 201.814 70.1868 201.814 67.1821C201.814 64.1758 202.286 61.3205 203.236 58.6154C204.182 55.9102 205.554 53.5571 207.35 51.5522C209.145 49.5495 211.288 47.9451 213.782 46.7427C216.275 45.5404 219.068 44.9396 222.16 44.9396C225.251 44.9396 228.07 45.5404 230.614 46.7427C233.156 47.9451 235.326 49.5487 237.122 51.5522C238.917 53.5571 240.312 55.9102 241.31 58.6154C242.306 61.3205 242.806 64.1758 242.806 67.1821C242.805 70.1868 242.305 73.0421 241.31 75.7472Z"
|
</span>
|
||||||
fill="currentColor"
|
</li>
|
||||||
/>
|
<li
|
||||||
<path
|
class="relative flex items-center gap-4 py-2 before:absolute before:top-0 before:bottom-1/2 before:left-[0.4rem] before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A]"
|
||||||
d="M438 -3H421.694V102.197H438V-3Z"
|
>
|
||||||
fill="currentColor"
|
<span class="relative bg-white py-1 dark:bg-[#161615]">
|
||||||
/>
|
<span
|
||||||
<path
|
class="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#e3e3e0] bg-[#FDFDFC] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] dark:border-[#3E3E3A] dark:bg-[#161615]"
|
||||||
d="M139.43 102.197H155.735V48.2834H183.712V32.1665H139.43V102.197Z"
|
>
|
||||||
fill="currentColor"
|
<span
|
||||||
/>
|
class="h-1.5 w-1.5 rounded-full bg-[#dbdbd7] dark:bg-[#3E3E3A]"
|
||||||
<path
|
/>
|
||||||
d="M324.49 32.1665L303.995 85.794L283.498 32.1665H266.983L293.748 102.197H314.242L341.006 32.1665H324.49Z"
|
</span>
|
||||||
fill="currentColor"
|
</span>
|
||||||
/>
|
<span>
|
||||||
<path
|
Watch video tutorials at
|
||||||
d="M376.571 30.3656C356.603 30.3656 340.797 46.8497 340.797 67.1828C340.797 89.6597 356.094 104 378.661 104C391.29 104 399.354 99.1488 409.206 88.5848L398.189 80.0226C398.183 80.031 389.874 90.9895 377.468 90.9895C363.048 90.9895 356.977 79.3111 356.977 73.269H411.075C413.917 50.1328 398.775 30.3656 376.571 30.3656ZM357.02 61.0967C357.145 59.7487 359.023 43.3761 376.442 43.3761C393.861 43.3761 395.978 59.7464 396.099 61.0967H357.02Z"
|
<a
|
||||||
fill="currentColor"
|
href="https://laracasts.com"
|
||||||
/>
|
target="_blank"
|
||||||
</svg>
|
class="ml-1 inline-flex items-center space-x-1 font-medium text-[#f53003] underline underline-offset-4 dark:text-[#FF4433]"
|
||||||
|
>
|
||||||
<!-- 13 -->
|
<span>Laracasts</span>
|
||||||
<svg
|
<svg
|
||||||
class="relative -mt-[6.6rem] -ml-8 w-[438px] max-w-none [--stroke-color:#1B1B18] lg:ml-0 dark:[--stroke-color:#FF750F]"
|
width="10"
|
||||||
viewBox="0 0 440 392"
|
height="11"
|
||||||
fill="none"
|
viewBox="0 0 10 11"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
fill="none"
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<g
|
class="h-2.5 w-2.5"
|
||||||
class="text-[#1B1B18] opacity-100 mix-blend-darken transition-all delay-300 duration-750 dark:text-black dark:mix-blend-normal starting:opacity-0"
|
>
|
||||||
>
|
<path
|
||||||
<mask
|
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
|
||||||
id="path-1-mask"
|
stroke="currentColor"
|
||||||
maskUnits="userSpaceOnUse"
|
stroke-linecap="square"
|
||||||
x="-0.328613"
|
/>
|
||||||
y="103"
|
</svg>
|
||||||
width="338"
|
</a>
|
||||||
height="299"
|
</span>
|
||||||
fill="black"
|
</li>
|
||||||
>
|
</ul>
|
||||||
<rect
|
<ul class="flex gap-3 text-sm leading-normal">
|
||||||
fill="white"
|
<li>
|
||||||
x="-0.328613"
|
<a
|
||||||
y="103"
|
href="https://cloud.laravel.com"
|
||||||
width="338"
|
target="_blank"
|
||||||
height="299"
|
class="inline-block rounded-sm border border-black bg-[#1b1b18] px-5 py-1.5 text-sm leading-normal text-white hover:border-black hover:bg-black dark:border-[#eeeeec] dark:bg-[#eeeeec] dark:text-[#1C1C1A] dark:hover:border-white dark:hover:bg-white"
|
||||||
/>
|
>
|
||||||
<path
|
Deploy now
|
||||||
d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z"
|
</a>
|
||||||
/>
|
</li>
|
||||||
<path
|
</ul>
|
||||||
d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z"
|
|
||||||
/>
|
|
||||||
</mask>
|
|
||||||
<path
|
|
||||||
d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-1-mask)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-1-mask)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g
|
|
||||||
class="text-[#F3BEC7] opacity-100 transition-all delay-400 duration-750 dark:text-[#4B0600] starting:opacity-0 motion-safe:starting:-translate-x-[26px]"
|
|
||||||
>
|
|
||||||
<mask
|
|
||||||
id="path-2-mask"
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="25.3357"
|
|
||||||
y="103"
|
|
||||||
width="338"
|
|
||||||
height="299"
|
|
||||||
fill="black"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
fill="white"
|
|
||||||
x="25.3357"
|
|
||||||
y="103"
|
|
||||||
width="338"
|
|
||||||
height="299"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z"
|
|
||||||
/>
|
|
||||||
</mask>
|
|
||||||
<path
|
|
||||||
d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-2-mask)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-2-mask)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g
|
|
||||||
class="text-[#F8B803] opacity-100 mix-blend-color transition-all delay-400 duration-750 dark:text-[#391800] dark:mix-blend-hard-light starting:opacity-0 motion-safe:starting:-translate-x-[51px]"
|
|
||||||
>
|
|
||||||
<mask
|
|
||||||
id="path-3-mask"
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="51"
|
|
||||||
y="103"
|
|
||||||
width="338"
|
|
||||||
height="299"
|
|
||||||
fill="black"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
fill="white"
|
|
||||||
x="51"
|
|
||||||
y="103"
|
|
||||||
width="338"
|
|
||||||
height="299"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z"
|
|
||||||
/>
|
|
||||||
</mask>
|
|
||||||
<path
|
|
||||||
d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-3-mask)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-3-mask)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g
|
|
||||||
class="text-[#F3BEC7] opacity-100 mix-blend-multiply transition-all delay-400 duration-750 dark:text-[#733000] dark:mix-blend-normal starting:opacity-0 motion-safe:starting:-translate-x-[78px]"
|
|
||||||
>
|
|
||||||
<mask
|
|
||||||
id="path-4-mask"
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="76.6643"
|
|
||||||
y="103"
|
|
||||||
width="338"
|
|
||||||
height="299"
|
|
||||||
fill="black"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
fill="white"
|
|
||||||
x="76.6643"
|
|
||||||
y="103"
|
|
||||||
width="338"
|
|
||||||
height="299"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z"
|
|
||||||
/>
|
|
||||||
</mask>
|
|
||||||
<path
|
|
||||||
d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-4-mask)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-4-mask)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g
|
|
||||||
class="text-[#F3BEC7] opacity-100 mix-blend-hard-light transition-all delay-400 duration-750 dark:text-[#4B0600] starting:opacity-0 motion-safe:starting:-translate-x-[102px]"
|
|
||||||
>
|
|
||||||
<mask
|
|
||||||
id="path-5-mask"
|
|
||||||
maskUnits="userSpaceOnUse"
|
|
||||||
x="102.329"
|
|
||||||
y="103"
|
|
||||||
width="338"
|
|
||||||
height="299"
|
|
||||||
fill="black"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
fill="white"
|
|
||||||
x="102.329"
|
|
||||||
y="103"
|
|
||||||
width="338"
|
|
||||||
height="299"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z"
|
|
||||||
/>
|
|
||||||
</mask>
|
|
||||||
<path
|
|
||||||
d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-5-mask)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z"
|
|
||||||
stroke="var(--stroke-color)"
|
|
||||||
stroke-width="2.4"
|
|
||||||
mask="url(#path-5-mask)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 rounded-t-lg shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] lg:rounded-t-none lg:rounded-r-lg dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d]"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden h-14.5 lg:block"></div>
|
<div
|
||||||
|
class="relative -mb-px aspect-[335/364] w-full shrink-0 overflow-hidden rounded-t-lg bg-[#fff2f2] lg:mb-0 lg:-ml-px lg:aspect-auto lg:w-[438px] lg:rounded-t-none lg:rounded-r-lg dark:bg-[#1D0002]"
|
||||||
|
>
|
||||||
|
<!-- Laravel Logo -->
|
||||||
|
<svg
|
||||||
|
class="w-full max-w-none translate-y-0 text-[#F53003] opacity-100 transition-all duration-750 dark:text-[#F61500] starting:opacity-0 motion-safe:starting:translate-y-6"
|
||||||
|
viewBox="0 0 438 104"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.2036 -3H0V102.197H49.5189V86.7187H17.2036V-3Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M110.256 41.6337C108.061 38.1275 104.945 35.3731 100.905 33.3681C96.8667 31.3647 92.8016 30.3618 88.7131 30.3618C83.4247 30.3618 78.5885 31.3389 74.201 33.2923C69.8111 35.2456 66.0474 37.928 62.9059 41.3333C59.7643 44.7401 57.3198 48.6726 55.5754 53.1293C53.8287 57.589 52.9572 62.274 52.9572 67.1813C52.9572 72.1925 53.8287 76.8995 55.5754 81.3069C57.3191 85.7173 59.7636 89.6241 62.9059 93.0293C66.0474 96.4361 69.8119 99.1155 74.201 101.069C78.5885 103.022 83.4247 103.999 88.7131 103.999C92.8016 103.999 96.8667 102.997 100.905 100.994C104.945 98.9911 108.061 96.2359 110.256 92.7282V102.195H126.563V32.1642H110.256V41.6337ZM108.76 75.7472C107.762 78.4531 106.366 80.8078 104.572 82.8112C102.776 84.8161 100.606 86.4183 98.0637 87.6206C95.5202 88.823 92.7004 89.4238 89.6103 89.4238C86.5178 89.4238 83.7252 88.823 81.2324 87.6206C78.7388 86.4183 76.5949 84.8161 74.7998 82.8112C73.004 80.8078 71.6319 78.4531 70.6856 75.7472C69.7356 73.0421 69.2644 70.1868 69.2644 67.1821C69.2644 64.1758 69.7356 61.3205 70.6856 58.6154C71.6319 55.9102 73.004 53.5571 74.7998 51.5522C76.5949 49.5495 78.738 47.9451 81.2324 46.7427C83.7252 45.5404 86.5178 44.9396 89.6103 44.9396C92.7012 44.9396 95.5202 45.5404 98.0637 46.7427C100.606 47.9451 102.776 49.5487 104.572 51.5522C106.367 53.5571 107.762 55.9102 108.76 58.6154C109.756 61.3205 110.256 64.1758 110.256 67.1821C110.256 70.1868 109.756 73.0421 108.76 75.7472Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M242.805 41.6337C240.611 38.1275 237.494 35.3731 233.455 33.3681C229.416 31.3647 225.351 30.3618 221.262 30.3618C215.974 30.3618 211.138 31.3389 206.75 33.2923C202.36 35.2456 198.597 37.928 195.455 41.3333C192.314 44.7401 189.869 48.6726 188.125 53.1293C186.378 57.589 185.507 62.274 185.507 67.1813C185.507 72.1925 186.378 76.8995 188.125 81.3069C189.868 85.7173 192.313 89.6241 195.455 93.0293C198.597 96.4361 202.361 99.1155 206.75 101.069C211.138 103.022 215.974 103.999 221.262 103.999C225.351 103.999 229.416 102.997 233.455 100.994C237.494 98.9911 240.611 96.2359 242.805 92.7282V102.195H259.112V32.1642H242.805V41.6337ZM241.31 75.7472C240.312 78.4531 238.916 80.8078 237.122 82.8112C235.326 84.8161 233.156 86.4183 230.614 87.6206C228.07 88.823 225.251 89.4238 222.16 89.4238C219.068 89.4238 216.275 88.823 213.782 87.6206C211.289 86.4183 209.145 84.8161 207.35 82.8112C205.554 80.8078 204.182 78.4531 203.236 75.7472C202.286 73.0421 201.814 70.1868 201.814 67.1821C201.814 64.1758 202.286 61.3205 203.236 58.6154C204.182 55.9102 205.554 53.5571 207.35 51.5522C209.145 49.5495 211.288 47.9451 213.782 46.7427C216.275 45.5404 219.068 44.9396 222.16 44.9396C225.251 44.9396 228.07 45.5404 230.614 46.7427C233.156 47.9451 235.326 49.5487 237.122 51.5522C238.917 53.5571 240.312 55.9102 241.31 58.6154C242.306 61.3205 242.806 64.1758 242.806 67.1821C242.805 70.1868 242.305 73.0421 241.31 75.7472Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M438 -3H421.694V102.197H438V-3Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M139.43 102.197H155.735V48.2834H183.712V32.1665H139.43V102.197Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M324.49 32.1665L303.995 85.794L283.498 32.1665H266.983L293.748 102.197H314.242L341.006 32.1665H324.49Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M376.571 30.3656C356.603 30.3656 340.797 46.8497 340.797 67.1828C340.797 89.6597 356.094 104 378.661 104C391.29 104 399.354 99.1488 409.206 88.5848L398.189 80.0226C398.183 80.031 389.874 90.9895 377.468 90.9895C363.048 90.9895 356.977 79.3111 356.977 73.269H411.075C413.917 50.1328 398.775 30.3656 376.571 30.3656ZM357.02 61.0967C357.145 59.7487 359.023 43.3761 376.442 43.3761C393.861 43.3761 395.978 59.7464 396.099 61.0967H357.02Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- 13 -->
|
||||||
|
<svg
|
||||||
|
class="relative -mt-[6.6rem] -ml-8 w-[438px] max-w-none [--stroke-color:#1B1B18] lg:ml-0 dark:[--stroke-color:#FF750F]"
|
||||||
|
viewBox="0 0 440 392"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
class="text-[#1B1B18] opacity-100 mix-blend-darken transition-all delay-300 duration-750 dark:text-black dark:mix-blend-normal starting:opacity-0"
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="path-1-mask"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="-0.328613"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
fill="black"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
fill="white"
|
||||||
|
x="-0.328613"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-1-mask)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-1-mask)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
class="text-[#F3BEC7] opacity-100 transition-all delay-400 duration-750 dark:text-[#4B0600] starting:opacity-0 motion-safe:starting:-translate-x-[26px]"
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="path-2-mask"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="25.3357"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
fill="black"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
fill="white"
|
||||||
|
x="25.3357"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-2-mask)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-2-mask)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
class="text-[#F8B803] opacity-100 mix-blend-color transition-all delay-400 duration-750 dark:text-[#391800] dark:mix-blend-hard-light starting:opacity-0 motion-safe:starting:-translate-x-[51px]"
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="path-3-mask"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="51"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
fill="black"
|
||||||
|
>
|
||||||
|
<rect fill="white" x="51" y="103" width="338" height="299" />
|
||||||
|
<path
|
||||||
|
d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"
|
||||||
|
/>
|
||||||
|
<path d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z" />
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-3-mask)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-3-mask)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
class="text-[#F3BEC7] opacity-100 mix-blend-multiply transition-all delay-400 duration-750 dark:text-[#733000] dark:mix-blend-normal starting:opacity-0 motion-safe:starting:-translate-x-[78px]"
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="path-4-mask"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="76.6643"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
fill="black"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
fill="white"
|
||||||
|
x="76.6643"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-4-mask)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-4-mask)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
class="text-[#F3BEC7] opacity-100 mix-blend-hard-light transition-all delay-400 duration-750 dark:text-[#4B0600] starting:opacity-0 motion-safe:starting:-translate-x-[102px]"
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="path-5-mask"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="102.329"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
fill="black"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
fill="white"
|
||||||
|
x="102.329"
|
||||||
|
y="103"
|
||||||
|
width="338"
|
||||||
|
height="299"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-5-mask)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z"
|
||||||
|
stroke="var(--stroke-color)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
mask="url(#path-5-mask)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 rounded-t-lg shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] lg:rounded-t-none lg:rounded-r-lg dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d]"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hidden h-14.5 lg:block"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,63 +7,59 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { store } from '@/routes/password/confirm';
|
import { store } from '@/routes/password/confirm';
|
||||||
import {
|
import {
|
||||||
index as confirmOptions,
|
index as confirmOptions,
|
||||||
store as confirmStore,
|
store as confirmStore,
|
||||||
} from '@/actions/Laravel/Passkeys/Http/Controllers/PasskeyConfirmationController';
|
} from '@/actions/Laravel/Passkeys/Http/Controllers/PasskeyConfirmationController';
|
||||||
import PasskeyVerify from '@/components/PasskeyVerify.vue';
|
import PasskeyVerify from '@/components/PasskeyVerify.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
title: 'Confirm password',
|
title: 'Confirm password',
|
||||||
description:
|
description:
|
||||||
'This is a secure area of the application. Please confirm your password before continuing.',
|
'This is a secure area of the application. Please confirm your password before continuing.',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Confirm password" />
|
<Head title="Confirm password" />
|
||||||
|
|
||||||
<PasskeyVerify
|
<PasskeyVerify
|
||||||
:routes="{
|
:routes="{
|
||||||
options: confirmOptions(),
|
options: confirmOptions(),
|
||||||
submit: confirmStore(),
|
submit: confirmStore(),
|
||||||
}"
|
}"
|
||||||
label="Confirm with passkey"
|
label="Confirm with passkey"
|
||||||
loading-label="Confirming..."
|
loading-label="Confirming..."
|
||||||
separator="Or confirm with password"
|
separator="Or confirm with password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form
|
<Form v-bind="store.form()" reset-on-success v-slot="{ errors, processing }">
|
||||||
v-bind="store.form()"
|
<div class="space-y-6">
|
||||||
reset-on-success
|
<div class="grid gap-2">
|
||||||
v-slot="{ errors, processing }"
|
<Label htmlFor="password">Password</Label>
|
||||||
>
|
<PasswordInput
|
||||||
<div class="space-y-6">
|
id="password"
|
||||||
<div class="grid gap-2">
|
name="password"
|
||||||
<Label htmlFor="password">Password</Label>
|
class="mt-1 block w-full"
|
||||||
<PasswordInput
|
required
|
||||||
id="password"
|
autocomplete="current-password"
|
||||||
name="password"
|
autofocus
|
||||||
class="mt-1 block w-full"
|
/>
|
||||||
required
|
|
||||||
autocomplete="current-password"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputError :message="errors.password" />
|
<InputError :message="errors.password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
data-test="confirm-password-button"
|
data-test="confirm-password-button"
|
||||||
>
|
>
|
||||||
<Spinner v-if="processing" />
|
<Spinner v-if="processing" />
|
||||||
Confirm password
|
Confirm password
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,57 +10,57 @@ import { login } from '@/routes';
|
|||||||
import { email } from '@/routes/password';
|
import { email } from '@/routes/password';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
title: 'Forgot password',
|
title: 'Forgot password',
|
||||||
description: 'Enter your email to receive a password reset link',
|
description: 'Enter your email to receive a password reset link',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
status?: string;
|
status?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Forgot password" />
|
<Head title="Forgot password" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="status"
|
v-if="status"
|
||||||
class="mb-4 text-center text-sm font-medium text-green-600"
|
class="mb-4 text-center text-sm font-medium text-green-600"
|
||||||
>
|
>
|
||||||
{{ status }}
|
{{ status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<Form v-bind="email.form()" v-slot="{ errors, processing }">
|
<Form v-bind="email.form()" v-slot="{ errors, processing }">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="email">Email address</Label>
|
<Label for="email">Email address</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autofocus
|
autofocus
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.email" />
|
<InputError :message="errors.email" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-6 flex items-center justify-start">
|
<div class="my-6 flex items-center justify-start">
|
||||||
<Button
|
<Button
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
data-test="email-password-reset-link-button"
|
data-test="email-password-reset-link-button"
|
||||||
>
|
>
|
||||||
<Spinner v-if="processing" />
|
<Spinner v-if="processing" />
|
||||||
Email password reset link
|
Email password reset link
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div class="text-muted-foreground space-x-1 text-center text-sm">
|
<div class="text-muted-foreground space-x-1 text-center text-sm">
|
||||||
<span>Or, return to</span>
|
<span>Or, return to</span>
|
||||||
<TextLink :href="login()">log in</TextLink>
|
<TextLink :href="login()">log in</TextLink>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,97 +14,97 @@ import { request } from '@/routes/password';
|
|||||||
import PasskeyVerify from '@/components/PasskeyVerify.vue';
|
import PasskeyVerify from '@/components/PasskeyVerify.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
title: 'Log in to your account',
|
title: 'Log in to your account',
|
||||||
description: 'Enter your email and password below to log in',
|
description: 'Enter your email and password below to log in',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
status?: string;
|
status?: string;
|
||||||
canResetPassword: boolean;
|
canResetPassword: boolean;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Log in" />
|
<Head title="Log in" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="status"
|
v-if="status"
|
||||||
class="mb-4 text-center text-sm font-medium text-green-600"
|
class="mb-4 text-center text-sm font-medium text-green-600"
|
||||||
>
|
>
|
||||||
{{ status }}
|
{{ status }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PasskeyVerify />
|
||||||
|
|
||||||
|
<Form
|
||||||
|
v-bind="store.form()"
|
||||||
|
:reset-on-success="['password']"
|
||||||
|
v-slot="{ errors, processing }"
|
||||||
|
class="flex flex-col gap-6"
|
||||||
|
>
|
||||||
|
<div class="grid gap-6">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="email">Email address</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
:tabindex="1"
|
||||||
|
autocomplete="email"
|
||||||
|
placeholder="email@example.com"
|
||||||
|
/>
|
||||||
|
<InputError :message="errors.email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label for="password">Password</Label>
|
||||||
|
<TextLink
|
||||||
|
v-if="canResetPassword"
|
||||||
|
:href="request()"
|
||||||
|
class="text-sm"
|
||||||
|
:tabindex="5"
|
||||||
|
>
|
||||||
|
Forgot your password?
|
||||||
|
</TextLink>
|
||||||
|
</div>
|
||||||
|
<PasswordInput
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
:tabindex="2"
|
||||||
|
autocomplete="current-password"
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
<InputError :message="errors.password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label for="remember" class="flex items-center space-x-3">
|
||||||
|
<Checkbox id="remember" name="remember" :tabindex="3" />
|
||||||
|
<span>Remember me</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
class="mt-4 w-full"
|
||||||
|
:tabindex="4"
|
||||||
|
:disabled="processing"
|
||||||
|
data-test="login-button"
|
||||||
|
>
|
||||||
|
<Spinner v-if="processing" />
|
||||||
|
Log in
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasskeyVerify />
|
<div class="text-muted-foreground text-center text-sm">
|
||||||
|
Don't have an account?
|
||||||
<Form
|
<TextLink :href="register()" :tabindex="5">Sign up</TextLink>
|
||||||
v-bind="store.form()"
|
</div>
|
||||||
:reset-on-success="['password']"
|
</Form>
|
||||||
v-slot="{ errors, processing }"
|
|
||||||
class="flex flex-col gap-6"
|
|
||||||
>
|
|
||||||
<div class="grid gap-6">
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label for="email">Email address</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
required
|
|
||||||
autofocus
|
|
||||||
:tabindex="1"
|
|
||||||
autocomplete="email"
|
|
||||||
placeholder="email@example.com"
|
|
||||||
/>
|
|
||||||
<InputError :message="errors.email" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<Label for="password">Password</Label>
|
|
||||||
<TextLink
|
|
||||||
v-if="canResetPassword"
|
|
||||||
:href="request()"
|
|
||||||
class="text-sm"
|
|
||||||
:tabindex="5"
|
|
||||||
>
|
|
||||||
Forgot your password?
|
|
||||||
</TextLink>
|
|
||||||
</div>
|
|
||||||
<PasswordInput
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
required
|
|
||||||
:tabindex="2"
|
|
||||||
autocomplete="current-password"
|
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
<InputError :message="errors.password" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<Label for="remember" class="flex items-center space-x-3">
|
|
||||||
<Checkbox id="remember" name="remember" :tabindex="3" />
|
|
||||||
<span>Remember me</span>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
class="mt-4 w-full"
|
|
||||||
:tabindex="4"
|
|
||||||
:disabled="processing"
|
|
||||||
data-test="login-button"
|
|
||||||
>
|
|
||||||
<Spinner v-if="processing" />
|
|
||||||
Log in
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-muted-foreground text-center text-sm">
|
|
||||||
Don't have an account?
|
|
||||||
<TextLink :href="register()" :tabindex="5">Sign up</TextLink>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,104 +11,104 @@ import { login } from '@/routes';
|
|||||||
import { store } from '@/routes/register';
|
import { store } from '@/routes/register';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
passwordRules: string;
|
passwordRules: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
title: 'Create an account',
|
title: 'Create an account',
|
||||||
description: 'Enter your details below to create your account',
|
description: 'Enter your details below to create your account',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Register" />
|
<Head title="Register" />
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
v-bind="store.form()"
|
v-bind="store.form()"
|
||||||
:reset-on-success="['password', 'password_confirmation']"
|
:reset-on-success="['password', 'password_confirmation']"
|
||||||
v-slot="{ errors, processing }"
|
v-slot="{ errors, processing }"
|
||||||
class="flex flex-col gap-6"
|
class="flex flex-col gap-6"
|
||||||
>
|
>
|
||||||
<div class="grid gap-6">
|
<div class="grid gap-6">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="name">Name</Label>
|
<Label for="name">Name</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autofocus
|
autofocus
|
||||||
:tabindex="1"
|
:tabindex="1"
|
||||||
autocomplete="name"
|
autocomplete="name"
|
||||||
name="name"
|
name="name"
|
||||||
placeholder="Full name"
|
placeholder="Full name"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.name" />
|
<InputError :message="errors.name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="email">Email address</Label>
|
<Label for="email">Email address</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
:tabindex="2"
|
:tabindex="2"
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.email" />
|
<InputError :message="errors.email" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password">Password</Label>
|
<Label for="password">Password</Label>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id="password"
|
id="password"
|
||||||
required
|
required
|
||||||
:tabindex="3"
|
:tabindex="3"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
:passwordrules="passwordRules"
|
:passwordrules="passwordRules"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.password" />
|
<InputError :message="errors.password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password_confirmation">Confirm password</Label>
|
<Label for="password_confirmation">Confirm password</Label>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id="password_confirmation"
|
id="password_confirmation"
|
||||||
required
|
required
|
||||||
:tabindex="4"
|
:tabindex="4"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
name="password_confirmation"
|
name="password_confirmation"
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
:passwordrules="passwordRules"
|
:passwordrules="passwordRules"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.password_confirmation" />
|
<InputError :message="errors.password_confirmation" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="mt-2 w-full"
|
class="mt-2 w-full"
|
||||||
tabindex="5"
|
tabindex="5"
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
data-test="register-user-button"
|
data-test="register-user-button"
|
||||||
>
|
>
|
||||||
<Spinner v-if="processing" />
|
<Spinner v-if="processing" />
|
||||||
Create account
|
Create account
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-muted-foreground text-center text-sm">
|
<div class="text-muted-foreground text-center text-sm">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<TextLink
|
<TextLink
|
||||||
:href="login()"
|
:href="login()"
|
||||||
class="underline underline-offset-4"
|
class="underline underline-offset-4"
|
||||||
:tabindex="6"
|
:tabindex="6"
|
||||||
>Log in</TextLink
|
>Log in</TextLink
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,81 +10,81 @@ import { Spinner } from '@/components/ui/spinner';
|
|||||||
import { update } from '@/routes/password';
|
import { update } from '@/routes/password';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
title: 'Reset password',
|
title: 'Reset password',
|
||||||
description: 'Please enter your new password below',
|
description: 'Please enter your new password below',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
token: string;
|
token: string;
|
||||||
email: string;
|
email: string;
|
||||||
passwordRules: string;
|
passwordRules: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inputEmail = ref(props.email);
|
const inputEmail = ref(props.email);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Reset password" />
|
<Head title="Reset password" />
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
v-bind="update.form()"
|
v-bind="update.form()"
|
||||||
:transform="(data) => ({ ...data, token, email })"
|
:transform="(data) => ({ ...data, token, email })"
|
||||||
:reset-on-success="['password', 'password_confirmation']"
|
:reset-on-success="['password', 'password_confirmation']"
|
||||||
v-slot="{ errors, processing }"
|
v-slot="{ errors, processing }"
|
||||||
>
|
>
|
||||||
<div class="grid gap-6">
|
<div class="grid gap-6">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="email">Email</Label>
|
<Label for="email">Email</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
v-model="inputEmail"
|
v-model="inputEmail"
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.email" class="mt-2" />
|
<InputError :message="errors.email" class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password">Password</Label>
|
<Label for="password">Password</Label>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
autofocus
|
autofocus
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
:passwordrules="passwordRules"
|
:passwordrules="passwordRules"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.password" />
|
<InputError :message="errors.password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password_confirmation"> Confirm password </Label>
|
<Label for="password_confirmation"> Confirm password </Label>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id="password_confirmation"
|
id="password_confirmation"
|
||||||
name="password_confirmation"
|
name="password_confirmation"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
:passwordrules="passwordRules"
|
:passwordrules="passwordRules"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.password_confirmation" />
|
<InputError :message="errors.password_confirmation" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="mt-4 w-full"
|
class="mt-4 w-full"
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
data-test="reset-password-button"
|
data-test="reset-password-button"
|
||||||
>
|
>
|
||||||
<Spinner v-if="processing" />
|
<Spinner v-if="processing" />
|
||||||
Reset password
|
Reset password
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import InputError from '@/components/InputError.vue';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from '@/components/ui/input-otp';
|
} from '@/components/ui/input-otp';
|
||||||
import { store } from '@/routes/two-factor/login';
|
import { store } from '@/routes/two-factor/login';
|
||||||
import type { TwoFactorConfigContent } from '@/types';
|
import type { TwoFactorConfigContent } from '@/types';
|
||||||
@@ -16,118 +16,118 @@ const showRecoveryInput = ref<boolean>(false);
|
|||||||
const code = ref<string>('');
|
const code = ref<string>('');
|
||||||
|
|
||||||
const authConfigContent = computed<TwoFactorConfigContent>(() => {
|
const authConfigContent = computed<TwoFactorConfigContent>(() => {
|
||||||
if (showRecoveryInput.value) {
|
if (showRecoveryInput.value) {
|
||||||
return {
|
|
||||||
title: 'Recovery code',
|
|
||||||
description:
|
|
||||||
'Please confirm access to your account by entering one of your emergency recovery codes.',
|
|
||||||
buttonText: 'login using an authentication code',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'Authentication code',
|
title: 'Recovery code',
|
||||||
description:
|
description:
|
||||||
'Enter the authentication code provided by your authenticator application.',
|
'Please confirm access to your account by entering one of your emergency recovery codes.',
|
||||||
buttonText: 'login using a recovery code',
|
buttonText: 'login using an authentication code',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'Authentication code',
|
||||||
|
description:
|
||||||
|
'Enter the authentication code provided by your authenticator application.',
|
||||||
|
buttonText: 'login using a recovery code',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
setLayoutProps({
|
setLayoutProps({
|
||||||
title: authConfigContent.value.title,
|
title: authConfigContent.value.title,
|
||||||
description: authConfigContent.value.description,
|
description: authConfigContent.value.description,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleRecoveryMode = (clearErrors: () => void): void => {
|
const toggleRecoveryMode = (clearErrors: () => void): void => {
|
||||||
showRecoveryInput.value = !showRecoveryInput.value;
|
showRecoveryInput.value = !showRecoveryInput.value;
|
||||||
clearErrors();
|
clearErrors();
|
||||||
code.value = '';
|
code.value = '';
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Two-factor authentication" />
|
<Head title="Two-factor authentication" />
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<template v-if="!showRecoveryInput">
|
<template v-if="!showRecoveryInput">
|
||||||
<Form
|
<Form
|
||||||
v-bind="store.form()"
|
v-bind="store.form()"
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
reset-on-error
|
reset-on-error
|
||||||
@error="code = ''"
|
@error="code = ''"
|
||||||
#default="{ errors, processing, clearErrors }"
|
#default="{ errors, processing, clearErrors }"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="code" :value="code" />
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center space-y-3 text-center"
|
||||||
|
>
|
||||||
|
<div class="flex w-full items-center justify-center">
|
||||||
|
<InputOTP
|
||||||
|
id="otp"
|
||||||
|
v-model="code"
|
||||||
|
:maxlength="6"
|
||||||
|
:disabled="processing"
|
||||||
|
autofocus
|
||||||
>
|
>
|
||||||
<input type="hidden" name="code" :value="code" />
|
<InputOTPGroup>
|
||||||
<div
|
<InputOTPSlot
|
||||||
class="flex flex-col items-center justify-center space-y-3 text-center"
|
v-for="index in 6"
|
||||||
>
|
:key="index"
|
||||||
<div class="flex w-full items-center justify-center">
|
:index="index - 1"
|
||||||
<InputOTP
|
|
||||||
id="otp"
|
|
||||||
v-model="code"
|
|
||||||
:maxlength="6"
|
|
||||||
:disabled="processing"
|
|
||||||
autofocus
|
|
||||||
>
|
|
||||||
<InputOTPGroup>
|
|
||||||
<InputOTPSlot
|
|
||||||
v-for="index in 6"
|
|
||||||
:key="index"
|
|
||||||
:index="index - 1"
|
|
||||||
/>
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</div>
|
|
||||||
<InputError :message="errors.code" />
|
|
||||||
</div>
|
|
||||||
<Button type="submit" class="w-full" :disabled="processing"
|
|
||||||
>Continue</Button
|
|
||||||
>
|
|
||||||
<div class="text-muted-foreground text-center text-sm">
|
|
||||||
<span>or you can </span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
|
||||||
@click="() => toggleRecoveryMode(clearErrors)"
|
|
||||||
>
|
|
||||||
{{ authConfigContent.buttonText }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<Form
|
|
||||||
v-bind="store.form()"
|
|
||||||
class="space-y-4"
|
|
||||||
reset-on-error
|
|
||||||
#default="{ errors, processing, clearErrors }"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
name="recovery_code"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter recovery code"
|
|
||||||
:autofocus="showRecoveryInput"
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.recovery_code" />
|
</InputOTPGroup>
|
||||||
<Button type="submit" class="w-full" :disabled="processing"
|
</InputOTP>
|
||||||
>Continue</Button
|
</div>
|
||||||
>
|
<InputError :message="errors.code" />
|
||||||
|
</div>
|
||||||
|
<Button type="submit" class="w-full" :disabled="processing"
|
||||||
|
>Continue</Button
|
||||||
|
>
|
||||||
|
<div class="text-muted-foreground text-center text-sm">
|
||||||
|
<span>or you can </span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||||
|
@click="() => toggleRecoveryMode(clearErrors)"
|
||||||
|
>
|
||||||
|
{{ authConfigContent.buttonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="text-muted-foreground text-center text-sm">
|
<template v-else>
|
||||||
<span>or you can </span>
|
<Form
|
||||||
<button
|
v-bind="store.form()"
|
||||||
type="button"
|
class="space-y-4"
|
||||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
reset-on-error
|
||||||
@click="() => toggleRecoveryMode(clearErrors)"
|
#default="{ errors, processing, clearErrors }"
|
||||||
>
|
>
|
||||||
{{ authConfigContent.buttonText }}
|
<Input
|
||||||
</button>
|
name="recovery_code"
|
||||||
</div>
|
type="text"
|
||||||
</Form>
|
placeholder="Enter recovery code"
|
||||||
</template>
|
:autofocus="showRecoveryInput"
|
||||||
</div>
|
required
|
||||||
|
/>
|
||||||
|
<InputError :message="errors.recovery_code" />
|
||||||
|
<Button type="submit" class="w-full" :disabled="processing"
|
||||||
|
>Continue</Button
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="text-muted-foreground text-center text-sm">
|
||||||
|
<span>or you can </span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||||
|
@click="() => toggleRecoveryMode(clearErrors)"
|
||||||
|
>
|
||||||
|
{{ authConfigContent.buttonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,41 +7,41 @@ import { logout } from '@/routes';
|
|||||||
import { send } from '@/routes/verification';
|
import { send } from '@/routes/verification';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
title: 'Email verification',
|
title: 'Email verification',
|
||||||
description:
|
description:
|
||||||
'Please verify your email address by clicking on the link we just emailed to you.',
|
'Please verify your email address by clicking on the link we just emailed to you.',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
status?: string;
|
status?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Email verification" />
|
<Head title="Email verification" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="status === 'verification-link-sent'"
|
v-if="status === 'verification-link-sent'"
|
||||||
class="mb-4 text-center text-sm font-medium text-green-600"
|
class="mb-4 text-center text-sm font-medium text-green-600"
|
||||||
>
|
>
|
||||||
A new verification link has been sent to the email address you provided
|
A new verification link has been sent to the email address you provided
|
||||||
during registration.
|
during registration.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
v-bind="send.form()"
|
v-bind="send.form()"
|
||||||
class="space-y-6 text-center"
|
class="space-y-6 text-center"
|
||||||
v-slot="{ processing }"
|
v-slot="{ processing }"
|
||||||
>
|
>
|
||||||
<Button :disabled="processing" variant="secondary">
|
<Button :disabled="processing" variant="secondary">
|
||||||
<Spinner v-if="processing" />
|
<Spinner v-if="processing" />
|
||||||
Resend verification email
|
Resend verification email
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<TextLink :href="logout()" as="button" class="mx-auto block text-sm">
|
<TextLink :href="logout()" as="button" class="mx-auto block text-sm">
|
||||||
Log out
|
Log out
|
||||||
</TextLink>
|
</TextLink>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,28 +5,28 @@ import Heading from '@/components/Heading.vue';
|
|||||||
import { edit } from '@/routes/appearance';
|
import { edit } from '@/routes/appearance';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
title: 'Appearance settings',
|
title: 'Appearance settings',
|
||||||
href: edit(),
|
href: edit(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Appearance settings" />
|
<Head title="Appearance settings" />
|
||||||
|
|
||||||
<h1 class="sr-only">Appearance settings</h1>
|
<h1 class="sr-only">Appearance settings</h1>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<Heading
|
<Heading
|
||||||
variant="small"
|
variant="small"
|
||||||
title="Appearance settings"
|
title="Appearance settings"
|
||||||
description="Update the appearance settings for your account"
|
description="Update the appearance settings for your account"
|
||||||
/>
|
/>
|
||||||
<AppearanceTabs />
|
<AppearanceTabs />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import { edit } from '@/routes/profile';
|
|||||||
import { send } from '@/routes/verification';
|
import { send } from '@/routes/verification';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
title: 'Profile settings',
|
title: 'Profile settings',
|
||||||
href: edit(),
|
href: edit(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = usePage();
|
const page = usePage();
|
||||||
@@ -28,78 +28,78 @@ const user = computed(() => page.props.auth.user);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Profile settings" />
|
<Head title="Profile settings" />
|
||||||
|
|
||||||
<h1 class="sr-only">Profile settings</h1>
|
<h1 class="sr-only">Profile settings</h1>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-6">
|
<div class="flex flex-col space-y-6">
|
||||||
<Heading
|
<Heading
|
||||||
variant="small"
|
variant="small"
|
||||||
title="Profile"
|
title="Profile"
|
||||||
description="Update your name and email address"
|
description="Update your name and email address"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
v-bind="ProfileController.update.form()"
|
||||||
|
class="space-y-6"
|
||||||
|
v-slot="{ errors, processing }"
|
||||||
|
>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
class="mt-1 block w-full"
|
||||||
|
name="name"
|
||||||
|
:default-value="user.name"
|
||||||
|
required
|
||||||
|
autocomplete="name"
|
||||||
|
placeholder="Full name"
|
||||||
/>
|
/>
|
||||||
|
<InputError class="mt-2" :message="errors.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Form
|
<div class="grid gap-2">
|
||||||
v-bind="ProfileController.update.form()"
|
<Label for="email">Email address</Label>
|
||||||
class="space-y-6"
|
<Input
|
||||||
v-slot="{ errors, processing }"
|
id="email"
|
||||||
|
type="email"
|
||||||
|
class="mt-1 block w-full"
|
||||||
|
name="email"
|
||||||
|
:default-value="user.email"
|
||||||
|
required
|
||||||
|
autocomplete="username"
|
||||||
|
placeholder="Email address"
|
||||||
|
/>
|
||||||
|
<InputError class="mt-2" :message="errors.email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="page.props.mustVerifyEmail && !user.email_verified_at">
|
||||||
|
<p class="text-muted-foreground -mt-4 text-sm">
|
||||||
|
Your email address is unverified.
|
||||||
|
<Link
|
||||||
|
:href="send()"
|
||||||
|
as="button"
|
||||||
|
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||||
|
>
|
||||||
|
Click here to re-send the verification email.
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="page.props.status === 'verification-link-sent'"
|
||||||
|
class="mt-2 text-sm font-medium text-green-600"
|
||||||
>
|
>
|
||||||
<div class="grid gap-2">
|
A new verification link has been sent to your email address.
|
||||||
<Label for="name">Name</Label>
|
</div>
|
||||||
<Input
|
</div>
|
||||||
id="name"
|
|
||||||
class="mt-1 block w-full"
|
|
||||||
name="name"
|
|
||||||
:default-value="user.name"
|
|
||||||
required
|
|
||||||
autocomplete="name"
|
|
||||||
placeholder="Full name"
|
|
||||||
/>
|
|
||||||
<InputError class="mt-2" :message="errors.name" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<Label for="email">Email address</Label>
|
<Button :disabled="processing" data-test="update-profile-button"
|
||||||
<Input
|
>Save</Button
|
||||||
id="email"
|
>
|
||||||
type="email"
|
</div>
|
||||||
class="mt-1 block w-full"
|
</Form>
|
||||||
name="email"
|
</div>
|
||||||
:default-value="user.email"
|
|
||||||
required
|
|
||||||
autocomplete="username"
|
|
||||||
placeholder="Email address"
|
|
||||||
/>
|
|
||||||
<InputError class="mt-2" :message="errors.email" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="page.props.mustVerifyEmail && !user.email_verified_at">
|
<DeleteUser />
|
||||||
<p class="text-muted-foreground -mt-4 text-sm">
|
|
||||||
Your email address is unverified.
|
|
||||||
<Link
|
|
||||||
:href="send()"
|
|
||||||
as="button"
|
|
||||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
|
||||||
>
|
|
||||||
Click here to re-send the verification email.
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="page.props.status === 'verification-link-sent'"
|
|
||||||
class="mt-2 text-sm font-medium text-green-600"
|
|
||||||
>
|
|
||||||
A new verification link has been sent to your email address.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<Button :disabled="processing" data-test="update-profile-button"
|
|
||||||
>Save</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DeleteUser />
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -21,100 +21,94 @@ type Props = {
|
|||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
title: 'Security settings',
|
title: 'Security settings',
|
||||||
href: edit(),
|
href: edit(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Security settings" />
|
<Head title="Security settings" />
|
||||||
|
|
||||||
<h1 class="sr-only">Security settings</h1>
|
<h1 class="sr-only">Security settings</h1>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<Heading
|
<Heading
|
||||||
variant="small"
|
variant="small"
|
||||||
title="Update password"
|
title="Update password"
|
||||||
description="Ensure your account is using a long, random password to stay secure"
|
description="Ensure your account is using a long, random password to stay secure"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
v-bind="SecurityController.update.form()"
|
||||||
|
:options="{
|
||||||
|
preserveScroll: true,
|
||||||
|
}"
|
||||||
|
reset-on-success
|
||||||
|
:reset-on-error="[
|
||||||
|
'password',
|
||||||
|
'password_confirmation',
|
||||||
|
'current_password',
|
||||||
|
]"
|
||||||
|
class="space-y-6"
|
||||||
|
v-slot="{ errors, processing }"
|
||||||
|
>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="current_password">Current password</Label>
|
||||||
|
<PasswordInput
|
||||||
|
id="current_password"
|
||||||
|
name="current_password"
|
||||||
|
class="mt-1 block w-full"
|
||||||
|
autocomplete="current-password"
|
||||||
|
placeholder="Current password"
|
||||||
/>
|
/>
|
||||||
|
<InputError :message="errors.current_password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Form
|
<div class="grid gap-2">
|
||||||
v-bind="SecurityController.update.form()"
|
<Label for="password">New password</Label>
|
||||||
:options="{
|
<PasswordInput
|
||||||
preserveScroll: true,
|
id="password"
|
||||||
}"
|
name="password"
|
||||||
reset-on-success
|
class="mt-1 block w-full"
|
||||||
:reset-on-error="[
|
autocomplete="new-password"
|
||||||
'password',
|
placeholder="New password"
|
||||||
'password_confirmation',
|
:passwordrules="props.passwordRules"
|
||||||
'current_password',
|
/>
|
||||||
]"
|
<InputError :message="errors.password" />
|
||||||
class="space-y-6"
|
</div>
|
||||||
v-slot="{ errors, processing }"
|
|
||||||
>
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label for="current_password">Current password</Label>
|
|
||||||
<PasswordInput
|
|
||||||
id="current_password"
|
|
||||||
name="current_password"
|
|
||||||
class="mt-1 block w-full"
|
|
||||||
autocomplete="current-password"
|
|
||||||
placeholder="Current password"
|
|
||||||
/>
|
|
||||||
<InputError :message="errors.current_password" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password">New password</Label>
|
<Label for="password_confirmation">Confirm password</Label>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id="password"
|
id="password_confirmation"
|
||||||
name="password"
|
name="password_confirmation"
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
placeholder="New password"
|
placeholder="Confirm password"
|
||||||
:passwordrules="props.passwordRules"
|
:passwordrules="props.passwordRules"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.password" />
|
<InputError :message="errors.password_confirmation" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<Label for="password_confirmation">Confirm password</Label>
|
<Button :disabled="processing" data-test="update-password-button">
|
||||||
<PasswordInput
|
Save
|
||||||
id="password_confirmation"
|
</Button>
|
||||||
name="password_confirmation"
|
</div>
|
||||||
class="mt-1 block w-full"
|
</Form>
|
||||||
autocomplete="new-password"
|
</div>
|
||||||
placeholder="Confirm password"
|
|
||||||
:passwordrules="props.passwordRules"
|
|
||||||
/>
|
|
||||||
<InputError :message="errors.password_confirmation" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<ManageTwoFactor
|
||||||
<Button
|
:canManageTwoFactor="canManageTwoFactor"
|
||||||
:disabled="processing"
|
:requiresConfirmation="requiresConfirmation"
|
||||||
data-test="update-password-button"
|
:twoFactorEnabled="twoFactorEnabled"
|
||||||
>
|
/>
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ManageTwoFactor
|
<ManagePasskeys :canManagePasskeys="canManagePasskeys" :passkeys="passkeys" />
|
||||||
:canManageTwoFactor="canManageTwoFactor"
|
|
||||||
:requiresConfirmation="requiresConfirmation"
|
|
||||||
:twoFactorEnabled="twoFactorEnabled"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ManagePasskeys
|
|
||||||
:canManagePasskeys="canManagePasskeys"
|
|
||||||
:passkeys="passkeys"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,66 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import teamRoutes from '@/routes/teams';
|
||||||
|
import { Save } from '@lucide/vue';
|
||||||
|
import { Head, useForm } from '@inertiajs/vue3';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import InputError from '@/components/InputError.vue';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
layout: {
|
||||||
|
breadcrumbs: [
|
||||||
|
{
|
||||||
|
title: 'Teams',
|
||||||
|
href: teamRoutes.index(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Erstellen',
|
||||||
|
href: teamRoutes.create(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
require_2fa: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
function submit(): void {
|
||||||
|
form.submit(teamRoutes.store());
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Head title="Teams" />
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-center p-16">
|
||||||
|
<form class="grid w-full gap-5 py-4" @submit.prevent="submit">
|
||||||
|
<div class="grid grid-cols-2 gap-5">
|
||||||
|
<div class="grid items-start gap-3">
|
||||||
|
<Label for="name">Name</Label>
|
||||||
|
<Input id="name" name="name" v-model="form.name" />
|
||||||
|
<InputError :message="form.errors.name" />
|
||||||
|
<InputError :message="(form.errors as any).slug" />
|
||||||
|
</div>
|
||||||
|
<div class="grid items-start gap-3">
|
||||||
|
<Label for="require_2fa">Require 2FA</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="require_2fa"
|
||||||
|
name="require_2fa"
|
||||||
|
v-model="form.require_2fa"
|
||||||
|
/>
|
||||||
|
<InputError :message="form.errors.require_2fa" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full justify-end">
|
||||||
|
<Button type="submit" class="w-fit" :disabled="form.processing">
|
||||||
|
<component :is="Save" />
|
||||||
|
Erstellen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,11 +1,71 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import teamRoutes from '@/routes/teams';
|
||||||
|
import { Save } from '@lucide/vue';
|
||||||
|
import { Head, useForm } from '@inertiajs/vue3';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import InputError from '@/components/InputError.vue';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Team } from '@/types/models';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
team: Team;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
layout: {
|
||||||
|
breadcrumbs: [
|
||||||
|
{
|
||||||
|
title: 'Teams',
|
||||||
|
href: teamRoutes.index(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: props.team.name,
|
||||||
|
href: teamRoutes.create(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
name: props.team.name,
|
||||||
|
require_2fa: props.team.multi_factor_required,
|
||||||
|
});
|
||||||
|
|
||||||
|
function submit(): void {
|
||||||
|
form.patch(teamRoutes.patch({ team: props.team.slug }).url);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Head title="Teams" />
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-center p-16">
|
||||||
|
<form class="grid w-full gap-5 py-4" @submit.prevent="submit">
|
||||||
|
<div class="grid grid-cols-2 gap-5">
|
||||||
|
<div class="grid items-start gap-3">
|
||||||
|
<Label for="name">Name</Label>
|
||||||
|
<Input id="name" v-model="form.name" name="name" />
|
||||||
|
<InputError :message="form.errors.name" />
|
||||||
|
<InputError :message="(form.errors as any).slug" />
|
||||||
|
</div>
|
||||||
|
<div class="grid items-start gap-3">
|
||||||
|
<Label for="require_2fa">Require 2FA</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="require_2fa"
|
||||||
|
v-model="form.require_2fa"
|
||||||
|
name="require_2fa"
|
||||||
|
/>
|
||||||
|
<InputError :message="form.errors.require_2fa" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full justify-end">
|
||||||
|
<Button :disabled="form.processing" class="w-fit" type="submit">
|
||||||
|
<component :is="Save" />
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import teamRoutes from '@/routes/teams';
|
||||||
import teamRoutes from "@/routes/teams";
|
import {Head, Link, router, usePage} from '@inertiajs/vue3';
|
||||||
import {Head} from "@inertiajs/vue3";
|
import type {Team} from '@/types/models';
|
||||||
import type {Team} from "@/types/models";
|
import TextLink from '@/components/TextLink.vue';
|
||||||
import TextLink from "@/components/TextLink.vue";
|
import {Card, CardContent} from '@/components/ui/card';
|
||||||
import {Card, CardContent} from "@/components/ui/card";
|
import {Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||||
import {
|
import {Button} from '@/components/ui/button';
|
||||||
Table,
|
import {Eye, Pencil, Plus} from '@lucide/vue';
|
||||||
TableBody,
|
|
||||||
TableCaption,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table"
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
title: 'Meine Teams',
|
title: 'Teams',
|
||||||
href: teamRoutes.index(),
|
href: teamRoutes.index(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -27,14 +20,31 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
teams: Team[]
|
teams: Team[];
|
||||||
}>()
|
canCreateTeam: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const user = usePage().props.auth.user;
|
||||||
|
|
||||||
|
const openTeam = (team: Team) =>
|
||||||
|
router.visit(teamRoutes.show({team: team.slug}).url);
|
||||||
|
|
||||||
|
const canManageTeam = (team: Team) =>
|
||||||
|
team.members.find((member) => member.email === user.email)?.role === 'admin';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Teams"/>
|
<Head title="Teams"/>
|
||||||
|
|
||||||
<div class="flex justify-center p-16">
|
<div class="flex flex-col justify-center p-16">
|
||||||
|
<div v-if="canCreateTeam" class="flex w-full justify-end py-4">
|
||||||
|
<Link :href="teamRoutes.create()">
|
||||||
|
<Button>
|
||||||
|
<component :is="Plus"/>
|
||||||
|
Erstellen
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card class="w-full">
|
<Card class="w-full">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -49,19 +59,31 @@ defineProps<{
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow
|
<TableRow
|
||||||
v-if="teams.length > 0"
|
|
||||||
v-for="team in teams"
|
v-for="team in teams"
|
||||||
|
v-if="teams.length > 0"
|
||||||
|
:key="team.slug"
|
||||||
|
role="link"
|
||||||
|
tabindex="0"
|
||||||
|
@click="openTeam(team)"
|
||||||
>
|
>
|
||||||
<TableCell>{{ team.name }}</TableCell>
|
<TableCell>{{ team.name }}</TableCell>
|
||||||
<TableCell>{{ team.slug }}</TableCell>
|
<TableCell>{{ team.slug }}</TableCell>
|
||||||
<TableCell>{{ team.members.length }}</TableCell>
|
<TableCell>{{ team.members.length }}</TableCell>
|
||||||
<TableCell>{{ team.multi_factor_required }}</TableCell>
|
<TableCell>{{ team.multi_factor_required ? 'Ja' : 'Nein' }}</TableCell>
|
||||||
|
<Link
|
||||||
|
:href="(canManageTeam(team) ? teamRoutes.edit : teamRoutes.show)({ team: team.slug })"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<component :is="canManageTeam(team) ? Pencil : Eye" size="18"/>
|
||||||
|
</TableCell>
|
||||||
|
</Link>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableCaption v-if="teams.length <= 0">
|
<TableCaption v-if="teams.length <= 0">
|
||||||
Du bist noch von keinem Team mitglied.
|
Du bist noch von keinem Team mitglied. Tritt einem Team bei oder
|
||||||
Tritt einem Team bei oder
|
<TextLink :href="teamRoutes.create()">erstelle eins.</TextLink>
|
||||||
<TextLink href="{{ route('teams.create') }}">erstelle eins.</TextLink>
|
|
||||||
</TableCaption>
|
</TableCaption>
|
||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import teamRoutes from '@/routes/teams';
|
||||||
|
import {Head, Link, setLayoutProps} from '@inertiajs/vue3';
|
||||||
|
import type {Team} from '@/types/models';
|
||||||
|
import {Card, CardContent} from '@/components/ui/card';
|
||||||
|
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||||
|
import {Button} from '@/components/ui/button';
|
||||||
|
import {Pencil} from '@lucide/vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
team: Team;
|
||||||
|
canManageTeam: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
setLayoutProps({
|
||||||
|
breadcrumbs: [
|
||||||
|
{
|
||||||
|
title: 'Teams',
|
||||||
|
href: teamRoutes.index(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: props.team.name,
|
||||||
|
href: teamRoutes.index(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head title="Teams"/>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-center p-16">
|
||||||
|
<div v-if="canManageTeam" class="flex w-full justify-end py-4">
|
||||||
|
<Link :href="teamRoutes.create()">
|
||||||
|
<Button>
|
||||||
|
<component :is="Pencil"/>
|
||||||
|
Bearbeiten
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card class="w-full mb-8">
|
||||||
|
<CardContent class="flex flex-col">
|
||||||
|
<h1 class="font-semibold text-2xl">{{ team.name }}</h1>
|
||||||
|
<Table>
|
||||||
|
<TableBody class="[&_td]:py-0 [&_tr]:border-0 text-gray-500 text-sm">
|
||||||
|
<TableRow>
|
||||||
|
<TableCell class="font-bold ">Slug:</TableCell>
|
||||||
|
<TableCell>{{ team.slug }}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell class="font-bold ">Erfordert 2FA:</TableCell>
|
||||||
|
<TableCell>{{ team.multi_factor_required ? 'Ja' : 'Nein' }}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell class="font-bold ">Erstellt am:</TableCell>
|
||||||
|
<TableCell>{{ team.created_at }}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<h2 class="font-semibold text-lg">Mitglieder</h2>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>2FA</TableHead>
|
||||||
|
<TableHead>Rolle</TableHead>
|
||||||
|
<TableHead>Beigetreten Am</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow v-for="member in team.members">
|
||||||
|
<TableCell>{{ member.name }}</TableCell>
|
||||||
|
<TableCell>{{ member.email }}</TableCell>
|
||||||
|
<TableCell>{{ member.has_multi_factor ? 'Ja' : 'Nein' }}</TableCell>
|
||||||
|
<TableCell>{{ member.role }}</TableCell>
|
||||||
|
<TableCell>{{ member.joined_at }}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+18
-18
@@ -1,29 +1,29 @@
|
|||||||
export type User = {
|
export type User = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
email_verified_at: string | null;
|
email_verified_at: string | null;
|
||||||
two_factor_enabled?: boolean;
|
two_factor_enabled?: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Auth = {
|
export type Auth = {
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Passkey = {
|
export type Passkey = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
authenticator: string | null;
|
authenticator: string | null;
|
||||||
created_at_diff: string;
|
created_at_diff: string;
|
||||||
last_used_at_diff: string | null;
|
last_used_at_diff: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TwoFactorConfigContent = {
|
export type TwoFactorConfigContent = {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+21
-21
@@ -2,32 +2,32 @@ import type { Auth } from '@/types/auth';
|
|||||||
|
|
||||||
// Extend ImportMeta interface for Vite...
|
// Extend ImportMeta interface for Vite...
|
||||||
declare module 'vite/client' {
|
declare module 'vite/client' {
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APP_NAME: string;
|
readonly VITE_APP_NAME: string;
|
||||||
[key: string]: string | boolean | undefined;
|
[key: string]: string | boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
readonly glob: <T>(pattern: string) => Record<string, () => Promise<T>>;
|
readonly glob: <T>(pattern: string) => Record<string, () => Promise<T>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@inertiajs/core' {
|
declare module '@inertiajs/core' {
|
||||||
export interface InertiaConfig {
|
export interface InertiaConfig {
|
||||||
sharedPageProps: {
|
sharedPageProps: {
|
||||||
name: string;
|
name: string;
|
||||||
auth: Auth;
|
auth: Auth;
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
$inertia: typeof Router;
|
$inertia: typeof Router;
|
||||||
$page: Page;
|
$page: Page;
|
||||||
$headManager: ReturnType<typeof createHeadManager>;
|
$headManager: ReturnType<typeof createHeadManager>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ export interface Team {
|
|||||||
slug: string;
|
slug: string;
|
||||||
members: TeamMember[];
|
members: TeamMember[];
|
||||||
multi_factor_required: boolean;
|
multi_factor_required: boolean;
|
||||||
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TeamMemberRole = 'admin' | 'member'
|
export type TeamMemberRole = 'admin' | 'member';
|
||||||
|
|
||||||
export interface TeamMember {
|
export interface TeamMember {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
has_multi_factor: boolean;
|
has_multi_factor: boolean;
|
||||||
role: TeamMemberRole;
|
role: TeamMemberRole;
|
||||||
|
joined_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import type { InertiaLinkProps } from '@inertiajs/vue3';
|
|||||||
import type { LucideIcon } from '@lucide/vue';
|
import type { LucideIcon } from '@lucide/vue';
|
||||||
|
|
||||||
export type BreadcrumbItem = {
|
export type BreadcrumbItem = {
|
||||||
title: string;
|
title: string;
|
||||||
href: NonNullable<InertiaLinkProps['href']>;
|
href: NonNullable<InertiaLinkProps['href']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NavItem = {
|
export type NavItem = {
|
||||||
title: string;
|
title: string;
|
||||||
href: NonNullable<InertiaLinkProps['href']>;
|
href: NonNullable<InertiaLinkProps['href']>;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ export type ResolvedAppearance = 'light' | 'dark';
|
|||||||
export type AppVariant = 'header' | 'sidebar';
|
export type AppVariant = 'header' | 'sidebar';
|
||||||
|
|
||||||
export type FlashToast = {
|
export type FlashToast = {
|
||||||
type: 'success' | 'info' | 'warning' | 'error';
|
type: 'success' | 'info' | 'warning' | 'error';
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import type { DefineComponent } from 'vue';
|
import type { DefineComponent } from 'vue';
|
||||||
const component: DefineComponent;
|
const component: DefineComponent;
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -10,7 +10,11 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
|
|
||||||
Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () {
|
Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () {
|
||||||
Route::get('/', 'index')->name('index');
|
Route::get('/', 'index')->name('index');
|
||||||
Route::get('/create', 'create')->name('create');
|
Route::middleware('verified')->get('/create', 'create')->name('create');
|
||||||
|
Route::middleware('verified')->post('/', 'store')->name('store');
|
||||||
|
Route::get('/{team:slug}', 'show')->name('show');
|
||||||
|
Route::get('/edit/{team:slug}', 'edit')->name('edit');
|
||||||
|
Route::patch('/{team:slug}', 'patch')->name('patch');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
+77
-70
@@ -7,79 +7,86 @@ import { bunny } from 'laravel-vite-plugin/fonts';
|
|||||||
import { defineConfig, lazyPlugins } from 'vite-plus';
|
import { defineConfig, lazyPlugins } from 'vite-plus';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: lazyPlugins(() => [
|
plugins: lazyPlugins(() => [
|
||||||
laravel({
|
laravel({
|
||||||
input: ['resources/css/app.css', 'resources/js/app.ts'],
|
input: ['resources/css/app.css', 'resources/js/app.ts'],
|
||||||
refresh: true,
|
refresh: true,
|
||||||
fonts: [
|
fonts: [
|
||||||
bunny('Instrument Sans', {
|
bunny('Instrument Sans', {
|
||||||
weights: [400, 500, 600],
|
weights: [400, 500, 600],
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
inertia(),
|
],
|
||||||
tailwindcss(),
|
}),
|
||||||
vue({
|
inertia(),
|
||||||
template: {
|
tailwindcss(),
|
||||||
transformAssetUrls: {
|
vue({
|
||||||
base: null,
|
template: {
|
||||||
includeAbsolute: false,
|
transformAssetUrls: {
|
||||||
},
|
base: null,
|
||||||
},
|
includeAbsolute: false,
|
||||||
}),
|
|
||||||
wayfinder({
|
|
||||||
formVariants: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
server: {
|
|
||||||
watch: {
|
|
||||||
ignored: [
|
|
||||||
'**/.agents/**',
|
|
||||||
'**/.claude/**',
|
|
||||||
'**/.cursor/**',
|
|
||||||
'**/.junie/**',
|
|
||||||
'**/vendor/**',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
wayfinder({
|
||||||
|
formVariants: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
ignored: [
|
||||||
|
'**/.agents/**',
|
||||||
|
'**/.claude/**',
|
||||||
|
'**/.cursor/**',
|
||||||
|
'**/.junie/**',
|
||||||
|
'**/vendor/**',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
lint: {
|
},
|
||||||
ignorePatterns: [
|
lint: {
|
||||||
'vendor/**',
|
ignorePatterns: [
|
||||||
'node_modules/**',
|
'vendor/**',
|
||||||
'public/**',
|
'node_modules/**',
|
||||||
'bootstrap/ssr/**',
|
'public/**',
|
||||||
'tailwind.config.js',
|
'bootstrap/ssr/**',
|
||||||
'resources/js/actions/**',
|
'tailwind.config.js',
|
||||||
'resources/js/components/ui/*',
|
'resources/js/actions/**',
|
||||||
'resources/js/routes/**',
|
'resources/js/components/ui/*',
|
||||||
'resources/js/wayfinder/**',
|
'resources/js/routes/**',
|
||||||
],
|
'resources/js/wayfinder/**',
|
||||||
options: {
|
],
|
||||||
denyWarnings: true,
|
options: {
|
||||||
typeAware: true,
|
denyWarnings: true,
|
||||||
},
|
typeAware: true,
|
||||||
},
|
},
|
||||||
fmt: {
|
},
|
||||||
printWidth: 80,
|
fmt: {
|
||||||
tabWidth: 4,
|
printWidth: 80,
|
||||||
singleQuote: true,
|
tabWidth: 2,
|
||||||
semi: true,
|
singleQuote: true,
|
||||||
singleAttributePerLine: false,
|
semi: true,
|
||||||
htmlWhitespaceSensitivity: 'css',
|
singleAttributePerLine: false,
|
||||||
ignorePatterns: [
|
htmlWhitespaceSensitivity: 'css',
|
||||||
'**/*.md',
|
ignorePatterns: [
|
||||||
'.github/**',
|
'**/.json',
|
||||||
'html/**',
|
'**/*.md',
|
||||||
'boost.json',
|
'.github/**',
|
||||||
'composer.json',
|
'html/**',
|
||||||
'docker-compose.yml',
|
'boost.json',
|
||||||
'docker/**',
|
'pint.json',
|
||||||
'resources/js/components/ui/*',
|
'components.json',
|
||||||
'resources/views/mail/*',
|
'package.json',
|
||||||
],
|
'tsconfig.json',
|
||||||
sortTailwindcss: {
|
'vite.config.ts',
|
||||||
functions: ['clsx', 'cn', 'cva'],
|
'pnpm-workspace.yaml',
|
||||||
entryPoint: 'resources/css/app.css',
|
'composer.json',
|
||||||
},
|
'docker-compose.yml',
|
||||||
|
'docker/**',
|
||||||
|
'resources/js/components/ui/*',
|
||||||
|
'resources/views/mail/*',
|
||||||
|
],
|
||||||
|
sortTailwindcss: {
|
||||||
|
functions: ['clsx', 'cn', 'cva'],
|
||||||
|
entryPoint: 'resources/css/app.css',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user