Initial Commit
tests / ci (push) Canceled after 0s

This commit is contained in:
Mark Gregory
2026-10-15 17:24:49 +01:00
parent 48a95478ef
commit 23de49aae0
27 changed files with 946 additions and 26 deletions
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureLodgeAdmin
{
public function handle(Request $request, Closure $next): Response
{
$admin = trim((string) config('lodge.admin_email', ''));
$email = trim((string) $request->user()?->email);
abort_unless($admin !== '' && $email !== '' && strcasecmp($admin, $email) === 0, 403);
return $next($request);
}
}
@@ -2,7 +2,7 @@
<flux:sidebar.profile
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
icon:trailing="chevrons-up-down"
icon:trailing="chevron-down"
data-test="sidebar-menu-button"
/>
@@ -1,5 +1,2 @@
<x-layouts::app.sidebar :title="$title ?? null">
<flux:main>
{{ $slot }}
</flux:main>
</x-layouts::app.sidebar>
{{-- Use the same Cresta 31 shell for Livewire settings and admin pages. --}}
@include('lodge.admin-layout', ['slot' => $slot, 'title' => $title ?? 'Account settings'])
@@ -24,7 +24,7 @@
<flux:tooltip :content="__('Repository')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="folder-git-2"
icon="folder"
href="https://github.com/laravel/livewire-starter-kit"
target="_blank"
:label="__('Repository')"
@@ -33,7 +33,7 @@
<flux:tooltip :content="__('Documentation')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="book-open-text"
icon="book-open"
href="https://laravel.com/docs/starter-kits#livewire"
target="_blank"
:label="__('Documentation')"
@@ -62,10 +62,10 @@
<flux:spacer />
<flux:sidebar.nav>
<flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
<flux:sidebar.item icon="folder" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:sidebar.item>
<flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
<flux:sidebar.item icon="book-open" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:sidebar.item>
</flux:sidebar.nav>
@@ -10,24 +10,56 @@
<flux:sidebar.collapse class="lg:hidden" />
</flux:sidebar.header>
@php
$adminEmail = trim((string) config('lodge.admin_email', ''));
$isLodgeAdmin = auth()->check()
&& $adminEmail !== ''
&& strcasecmp(trim((string) auth()->user()->email), $adminEmail) === 0;
@endphp
<flux:sidebar.nav>
<flux:sidebar.group :heading="__('Platform')" class="grid">
<flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:sidebar.item>
@if ($isLodgeAdmin)
<flux:sidebar.group heading="Overview" class="grid">
@if (Route::has('admin.dashboard'))
<flux:sidebar.item icon="home" :href="route('admin.dashboard')" :current="request()->routeIs('admin.dashboard')">Dashboard</flux:sidebar.item>
@endif
</flux:sidebar.group>
<flux:sidebar.group heading="Reservations" class="grid">
@if (Route::has('lodge.admin'))
<flux:sidebar.item icon="calendar" :href="route('lodge.admin')" :current="request()->routeIs('lodge.admin')">Bookings</flux:sidebar.item>
@endif
@if (Route::has('lodge.availability'))
<flux:sidebar.item icon="clock" :href="route('lodge.availability')">Availability</flux:sidebar.item>
@endif
</flux:sidebar.group>
<flux:sidebar.group heading="Lodge Management" class="grid">
@if (Route::has('admin.photos'))
<flux:sidebar.item icon="photo" :href="route('admin.photos')" :current="request()->routeIs('admin.photos')">Photo Gallery</flux:sidebar.item>
@endif
</flux:sidebar.group>
<flux:sidebar.group heading="System" class="grid">
@if (Route::has('admin.modules.index'))
<flux:sidebar.item icon="squares-2x2" :href="route('admin.modules.index')" :current="request()->routeIs('admin.modules.*')">Modules</flux:sidebar.item>
@elseif (Route::has('modules.index'))
<flux:sidebar.item icon="squares-2x2" :href="route('modules.index')">Modules</flux:sidebar.item>
@endif
</flux:sidebar.group>
@endif
<flux:sidebar.group heading="Account" class="grid">
@if (Route::has('profile.edit'))
<flux:sidebar.item icon="cog" :href="route('profile.edit')" :current="request()->routeIs('profile.edit')">Profile Settings</flux:sidebar.item>
@endif
</flux:sidebar.group>
</flux:sidebar.nav>
<flux:spacer />
<flux:sidebar.nav>
<flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:sidebar.item>
<flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:sidebar.item>
<flux:sidebar.item icon="home" :href="url('/')">View Website</flux:sidebar.item>
</flux:sidebar.nav>
<x-desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" />
@@ -1,3 +1,7 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="csrf-token" content="{{ csrf_token() }}"><title>@yield('title','Cresta 31 | Lodge escapes')</title><meta name="description" content="Escape to Cresta 31. Discover the lodge, check availability and request your stay."><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Playfair+Display:wght@500;600&display=swap" rel="stylesheet"><style>
:root{--ink:#1b312b;--muted:#697b70;--sand:#f6f3eb;--gold:#c49b5a;--white:#fff}*{box-sizing:border-box}body{margin:0;background:var(--sand);color:var(--ink);font-family:'DM Sans',sans-serif}a{color:inherit;text-decoration:none}h1,h2,h3{font-family:'Playfair Display',serif;font-weight:500}header{position:absolute;top:0;left:0;right:0;z-index:2;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:25px max(5vw,24px)}header .brand{font-family:'Playfair Display',serif;font-size:30px}header nav{display:flex;gap:25px;align-items:center}header a.button{background:#fff;color:var(--ink)}.button,button{display:inline-block;background:var(--ink);color:white;border:0;padding:15px 24px;border-radius:3px;font:600 14px 'DM Sans';cursor:pointer}.button:hover,button:hover{opacity:.88}.wrap{max-width:1200px;margin:auto;padding:0 24px}.hero{height:min(85vh,780px);min-height:530px;position:relative;display:flex;align-items:center;background:#233d34 url('/images/lodge/hero/photo-01.webp') center/cover;color:#fff}.hero:before{content:'';position:absolute;inset:0;background:linear-gradient(90deg,rgba(12,30,24,.7),rgba(12,30,24,.08))}.hero .content{position:relative;max-width:750px;padding:80px 0}.eyebrow{text-transform:uppercase;letter-spacing:.24em;font-size:12px;font-weight:700}.hero h1{font-size:clamp(50px,7vw,95px);line-height:1.05;margin:20px 0}.hero p{font-size:18px;max-width:480px;line-height:1.8}.section{padding:85px 0}.section h2{font-size:clamp(34px,4vw,54px);margin:10px 0 25px}.intro{display:grid;grid-template-columns:1fr 1fr;gap:70px;align-items:center}.intro p{line-height:1.9;color:var(--muted)}.intro img{width:100%;height:480px;object-fit:cover}.gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.gallery img{width:100%;height:275px;object-fit:cover;border-radius:3px}.booking{background:#fff;padding:34px;box-shadow:0 14px 60px #172e2220;border-radius:5px}.booking-grid{display:grid;grid-template-columns:repeat(3,1fr) auto;gap:15px;align-items:end}label{display:block;font-size:12px;text-transform:uppercase;letter-spacing:.12em;font-weight:700;margin-bottom:9px}input,select,textarea{display:block;width:100%;border:1px solid #d5ddd7;padding:14px;font:inherit;border-radius:3px;background:#fff;color:var(--ink)}.small{color:var(--muted);font-size:14px}.card{background:#fff;padding:36px;border-radius:5px;max-width:760px;margin:auto}.card h1{font-size:46px}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.full{grid-column:1/-1}.errors{background:#fff0ed;color:#8c3024;padding:16px;margin:16px 0}.subhero{background:var(--ink);color:#fff;padding:155px 24px 65px}.subhero h1{font-size:54px;margin:0}footer{background:#142820;color:#fff;padding:45px 0;margin-top:30px}footer .wrap{display:flex;justify-content:space-between}.price{font-size:30px;font-family:'Playfair Display',serif}table{border-collapse:collapse;width:100%;background:white}td,th{padding:13px;text-align:left;border-bottom:1px solid #ddd}@media(max-width:760px){header nav a:not(.button){display:none}.intro{grid-template-columns:1fr;gap:25px}.gallery{grid-template-columns:repeat(2,1fr)}.gallery img{height:180px}.booking-grid,.form-grid{grid-template-columns:1fr}.hero{min-height:600px}.section{padding:55px 0}footer .wrap{display:block}}
</style></head><body><header><a class="brand" href="{{ route('home') }}">Cresta 31<span style="color:#d9b985">.</span></a><nav><a href="{{ route('home') }}#the-lodge">The lodge</a><a href="{{ route('home') }}#gallery">Gallery</a><a class="button" href="{{ route('home') }}#book">Check availability</a>@auth @if(filled(config('lodge.admin_email')) && strcasecmp(trim(auth()->user()->email),trim(config('lodge.admin_email')))===0)<a href="{{ route('admin.dashboard') }}" style="border:1px solid rgba(255,255,255,.8);padding:10px 15px;border-radius:4px;font-weight:700">Admin dashboard →</a>@endif @endauth</nav></header>@yield('content')<footer><div class="wrap"><div><strong>Cresta 31</strong><p class="small">Your next lodge escape starts here.</p></div><div>© {{ date('Y') }} Cresta 31</div></div></footer></body></html>
</style></head><body><header><a class="brand" href="{{ route('home') }}">Cresta 31<span style="color:#d9b985">.</span></a><nav><a href="{{ route('home') }}#the-lodge">The lodge</a><a href="{{ route('home') }}#gallery">Gallery</a><a class="button" href="{{ route('home') }}#book">Check availability</a>@auth
@if(filled(config('lodge.admin_email')) && strcasecmp(trim((string) auth()->user()->email), trim((string) config('lodge.admin_email'))) === 0)
<a href="{{ route('admin.dashboard') }}" class="button" style="background:#d9b985;color:#1b312b">Administration</a>
@endif
@endauth</nav></header>@yield('content')<footer><div class="wrap"><div><strong>Cresta 31</strong><p class="small">Your next lodge escape starts here.</p></div><div>© {{ date('Y') }} Cresta 31</div></div></footer></body></html>
@@ -0,0 +1,20 @@
<div class="flex items-start max-md:flex-col">
<div class="me-10 w-full pb-4 md:w-[220px]">
<flux:navlist aria-label="{{ __('Settings') }}">
<flux:navlist.item :href="route('profile.edit')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
<flux:navlist.item :href="route('security.edit')" wire:navigate>{{ __('Security') }}</flux:navlist.item>
<flux:navlist.item :href="route('appearance.edit')" wire:navigate>{{ __('Appearance') }}</flux:navlist.item>
</flux:navlist>
</div>
<flux:separator class="md:hidden" />
<div class="flex-1 self-stretch max-md:pt-6">
<flux:heading>{{ $heading ?? '' }}</flux:heading>
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
<div class="mt-5 w-full max-w-lg">
{{ $slot }}
</div>
</div>
</div>
+2
View File
@@ -0,0 +1,2 @@
{{-- Use the same Cresta 31 shell for Livewire settings and admin pages. --}}
@include('lodge.admin-layout', ['slot' => $slot, 'title' => $title ?? 'Account settings'])
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden mr-2" icon="bars-2" inset="left" />
<x-app-logo href="{{ route('dashboard') }}" wire:navigate />
<flux:navbar class="-mb-px max-lg:hidden">
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:navbar.item>
</flux:navbar>
<flux:spacer />
<flux:navbar class="me-1.5 space-x-0.5 rtl:space-x-reverse py-0!">
<flux:tooltip :content="__('Search')" position="bottom">
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#" :label="__('Search')" />
</flux:tooltip>
<flux:tooltip :content="__('Repository')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="folder"
href="https://github.com/laravel/livewire-starter-kit"
target="_blank"
:label="__('Repository')"
/>
</flux:tooltip>
<flux:tooltip :content="__('Documentation')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="book-open"
href="https://laravel.com/docs/starter-kits#livewire"
target="_blank"
:label="__('Documentation')"
/>
</flux:tooltip>
</flux:navbar>
<x-desktop-user-menu />
</flux:header>
<!-- Mobile Menu -->
<flux:sidebar collapsible="mobile" sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.header>
<x-app-logo :sidebar="true" href="{{ route('dashboard') }}" wire:navigate />
<flux:sidebar.collapse class="in-data-flux-sidebar-on-desktop:not-in-data-flux-sidebar-collapsed-desktop:-mr-2" />
</flux:sidebar.header>
<flux:sidebar.nav>
<flux:sidebar.group :heading="__('Platform')">
<flux:sidebar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:sidebar.item>
</flux:sidebar.group>
</flux:sidebar.nav>
<flux:spacer />
<flux:sidebar.nav>
<flux:sidebar.item icon="folder" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:sidebar.item>
<flux:sidebar.item icon="book-open" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:sidebar.item>
</flux:sidebar.nav>
</flux:sidebar>
{{ $slot }}
@persist('toast')
<flux:toast.group>
<flux:toast />
</flux:toast.group>
@endpersist
@fluxScripts
</body>
</html>
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.header>
<x-app-logo :sidebar="true" href="{{ route('dashboard') }}" wire:navigate />
<flux:sidebar.collapse class="lg:hidden" />
</flux:sidebar.header>
@php
$adminEmail = trim((string) config('lodge.admin_email', ''));
$isLodgeAdmin = auth()->check()
&& $adminEmail !== ''
&& strcasecmp(trim((string) auth()->user()->email), $adminEmail) === 0;
@endphp
<flux:sidebar.nav>
@if ($isLodgeAdmin)
<flux:sidebar.group heading="Overview" class="grid">
@if (Route::has('admin.dashboard'))
<flux:sidebar.item icon="home" :href="route('admin.dashboard')" :current="request()->routeIs('admin.dashboard')">Dashboard</flux:sidebar.item>
@endif
</flux:sidebar.group>
<flux:sidebar.group heading="Reservations" class="grid">
@if (Route::has('lodge.admin'))
<flux:sidebar.item icon="calendar" :href="route('lodge.admin')" :current="request()->routeIs('lodge.admin')">Bookings</flux:sidebar.item>
@endif
@if (Route::has('lodge.availability'))
<flux:sidebar.item icon="clock" :href="route('lodge.availability')">Availability</flux:sidebar.item>
@endif
</flux:sidebar.group>
<flux:sidebar.group heading="Lodge Management" class="grid">
@if (Route::has('admin.photos'))
<flux:sidebar.item icon="photo" :href="route('admin.photos')" :current="request()->routeIs('admin.photos')">Photo Gallery</flux:sidebar.item>
@endif
</flux:sidebar.group>
<flux:sidebar.group heading="System" class="grid">
@if (Route::has('admin.modules.index'))
<flux:sidebar.item icon="squares-2x2" :href="route('admin.modules.index')" :current="request()->routeIs('admin.modules.*')">Modules</flux:sidebar.item>
@elseif (Route::has('modules.index'))
<flux:sidebar.item icon="squares-2x2" :href="route('modules.index')">Modules</flux:sidebar.item>
@endif
</flux:sidebar.group>
@endif
<flux:sidebar.group heading="Account" class="grid">
@if (Route::has('profile.edit'))
<flux:sidebar.item icon="cog" :href="route('profile.edit')" :current="request()->routeIs('profile.edit')">Profile Settings</flux:sidebar.item>
@endif
</flux:sidebar.group>
</flux:sidebar.nav>
<flux:spacer />
<flux:sidebar.nav>
<flux:sidebar.item icon="home" :href="url('/')">View Website</flux:sidebar.item>
</flux:sidebar.nav>
<x-desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" />
</flux:sidebar>
<!-- Mobile User Menu -->
<flux:header class="lg:hidden">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<flux:spacer />
<flux:dropdown position="top" align="end">
<flux:profile
:initials="auth()->user()->initials()"
icon-trailing="chevron-down"
/>
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<flux:avatar
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
/>
<div class="grid flex-1 text-start text-sm leading-tight">
<flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
<flux:text class="truncate">{{ auth()->user()->email }}</flux:text>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
{{ __('Settings') }}
</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item
as="button"
type="submit"
icon="arrow-right-start-on-rectangle"
class="w-full cursor-pointer"
data-test="logout-button"
>
{{ __('Log out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
{{ $slot }}
@persist('toast')
<flux:toast.group>
<flux:toast />
</flux:toast.group>
@endpersist
@fluxScripts
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
<x-layouts::auth.simple :title="$title ?? null">
{{ $slot }}
</x-layouts::auth.simple>
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-md flex-col gap-6">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8">{{ $slot }}</div>
</div>
</div>
</div>
</div>
@persist('toast')
<flux:toast.group>
<flux:toast />
</flux:toast.group>
@endpersist
@fluxScripts
</body>
</html>
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-2">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
{{ $slot }}
</div>
</div>
</div>
@persist('toast')
<flux:toast.group>
<flux:toast />
</flux:toast.group>
@endpersist
@fluxScripts
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
<div class="absolute inset-0 bg-neutral-900"></div>
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
<span class="flex h-10 w-10 items-center justify-center rounded-md">
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
</span>
{{ config('app.name', 'Laravel') }}
</a>
@php
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
@endphp
<div class="relative z-20 mt-auto">
<blockquote class="space-y-2">
<flux:heading size="lg">&ldquo;{{ trim($message) }}&rdquo;</flux:heading>
<footer><flux:heading>{{ trim($author) }}</flux:heading></footer>
</blockquote>
</div>
</div>
<div class="w-full lg:p-8">
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
{{ $slot }}
</div>
</div>
</div>
@persist('toast')
<flux:toast.group>
<flux:toast />
</flux:toast.group>
@endpersist
@fluxScripts
</body>
</html>
@@ -0,0 +1,13 @@
<section class="w-full">
@include('partials.settings-heading')
<flux:heading class="sr-only">{{ __('Appearance settings') }}</flux:heading>
<x-settings.layout :heading="__('Appearance')" :subheading=" __('Update the appearance settings for your account')">
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance">
<flux:radio value="light" icon="sun">{{ __('Light') }}</flux:radio>
<flux:radio value="dark" icon="moon">{{ __('Dark') }}</flux:radio>
<flux:radio value="system" icon="computer-desktop">{{ __('System') }}</flux:radio>
</flux:radio.group>
</x-settings.layout>
</section>
@@ -0,0 +1,34 @@
<section class="mt-10 space-y-6">
<div class="relative mb-5">
<flux:heading>{{ __('Delete account') }}</flux:heading>
<flux:subheading>{{ __('Delete your account and all of its resources') }}</flux:subheading>
</div>
<flux:modal.trigger name="confirm-user-deletion">
<flux:button variant="danger" x-data="" x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')">
{{ __('Delete account') }}
</flux:button>
</flux:modal.trigger>
<flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
<form method="POST" wire:submit="deleteUser" class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Are you sure you want to delete your account?') }}</flux:heading>
<flux:subheading>
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</flux:subheading>
</div>
<flux:input wire:model="password" :label="__('Password')" type="password" viewable />
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Cancel') }}</flux:button>
</flux:modal.close>
<flux:button variant="danger" type="submit">{{ __('Delete account') }}</flux:button>
</div>
</form>
</flux:modal>
</section>
@@ -0,0 +1,36 @@
<section class="w-full">
@include('partials.settings-heading')
<flux:heading class="sr-only">{{ __('Profile settings') }}</flux:heading>
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
<div>
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
@if ($this->hasUnverifiedEmail)
<div>
<flux:text class="mt-4">
{{ __('Your email address is unverified.') }}
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
{{ __('Click here to re-send the verification email.') }}
</flux:link>
</flux:text>
</div>
@endif
</div>
<div class="flex items-center gap-4">
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
</div>
</form>
@if ($this->showDeleteUser)
<livewire:settings.delete-user-form />
@endif
</x-settings.layout>
</section>
@@ -0,0 +1,327 @@
<section class="w-full">
@include('partials.settings-heading')
<flux:heading class="sr-only">{{ __('Security settings') }}</flux:heading>
<x-settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
<form method="POST" wire:submit="updatePassword" class="mt-6 space-y-6">
<flux:input
wire:model="current_password"
:label="__('Current password')"
type="password"
required
autocomplete="current-password"
viewable
/>
<flux:input
wire:model="password"
:label="__('New password')"
type="password"
required
autocomplete="new-password"
passwordrules="{{ \Illuminate\Validation\Rules\Password::defaults()->toPasswordRulesString() }}"
viewable
/>
<flux:input
wire:model="password_confirmation"
:label="__('Confirm password')"
type="password"
required
autocomplete="new-password"
passwordrules="{{ \Illuminate\Validation\Rules\Password::defaults()->toPasswordRulesString() }}"
viewable
/>
<div class="flex items-center gap-4">
<flux:button variant="primary" type="submit" data-test="update-password-button">{{ __('Save') }}</flux:button>
</div>
</form>
@if ($canManageTwoFactor)
<section class="mt-12">
<flux:heading>{{ __('Two-factor authentication') }}</flux:heading>
<flux:subheading>{{ __('Manage your two-factor authentication settings') }}</flux:subheading>
<div class="flex flex-col w-full mx-auto space-y-6 text-sm" wire:cloak>
@if ($twoFactorEnabled)
<div class="space-y-4">
<flux:text>
{{ __('You will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.') }}
</flux:text>
<div class="flex justify-start">
<flux:button
variant="danger"
wire:click="disable"
>
{{ __('Disable 2FA') }}
</flux:button>
</div>
<livewire:settings.two-factor.recovery-codes :$requiresConfirmation />
</div>
@else
<div class="space-y-4">
<flux:text variant="subtle">
{{ __('When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.') }}
</flux:text>
<flux:button
variant="primary"
wire:click="enable"
>
{{ __('Enable 2FA') }}
</flux:button>
</div>
@endif
</div>
</section>
@endif
@if ($canManageTwoFactor)
<flux:modal
name="two-factor-setup-modal"
class="max-w-md md:min-w-md"
@close="closeModal"
wire:model="showModal"
>
<div class="space-y-6">
<div class="flex flex-col items-center space-y-4">
<div class="p-0.5 w-auto rounded-full border border-stone-100 dark:border-stone-600 bg-white dark:bg-stone-800 shadow-sm">
<div class="p-2.5 rounded-full border border-stone-200 dark:border-stone-600 overflow-hidden bg-stone-100 dark:bg-stone-200 relative">
<div class="flex items-stretch absolute inset-0 w-full h-full divide-x [&>div]:flex-1 divide-stone-200 dark:divide-stone-300 justify-around opacity-50">
@for ($i = 1; $i <= 5; $i++)
<div></div>
@endfor
</div>
<div class="flex flex-col items-stretch absolute w-full h-full divide-y [&>div]:flex-1 inset-0 divide-stone-200 dark:divide-stone-300 justify-around opacity-50">
@for ($i = 1; $i <= 5; $i++)
<div></div>
@endfor
</div>
<flux:icon.qr-code class="relative z-20 dark:text-accent-foreground"/>
</div>
</div>
<div class="space-y-2 text-center">
<flux:heading size="lg">{{ $this->modalConfig['title'] }}</flux:heading>
<flux:text>{{ $this->modalConfig['description'] }}</flux:text>
</div>
</div>
@if ($showVerificationStep)
<div class="space-y-6">
<div
class="flex flex-col items-center space-y-3 justify-center"
x-data
x-init="$nextTick(() => $el.querySelector('input')?.focus())"
>
<flux:otp
name="code"
wire:model="code"
length="6"
label="OTP Code"
label:sr-only
class="mx-auto"
/>
</div>
<div class="flex items-center space-x-3">
<flux:button
variant="outline"
class="flex-1"
wire:click="resetVerification"
>
{{ __('Back') }}
</flux:button>
<flux:button
variant="primary"
class="flex-1"
wire:click="confirmTwoFactor"
x-bind:disabled="$wire.code.length < 6"
>
{{ __('Confirm') }}
</flux:button>
</div>
</div>
@else
@error('setupData')
<flux:callout variant="danger" icon="x-circle" heading="{{ $message }}"/>
@enderror
<div class="flex justify-center">
<div class="relative w-64 overflow-hidden border rounded-lg border-stone-200 dark:border-stone-700 aspect-square">
@empty($qrCodeSvg)
<div class="absolute inset-0 flex items-center justify-center bg-white dark:bg-stone-700 animate-pulse">
<flux:icon.loading/>
</div>
@else
<div x-data class="flex items-center justify-center h-full p-4">
<div
class="bg-white p-3 rounded"
:style="($flux.appearance === 'dark' || ($flux.appearance === 'system' && $flux.dark)) ? 'filter: invert(1) brightness(1.5)' : ''"
>
{!! $qrCodeSvg !!}
</div>
</div>
@endempty
</div>
</div>
<div>
<flux:button
:disabled="$errors->has('setupData')"
variant="primary"
class="w-full"
wire:click="showVerificationIfNecessary"
>
{{ $this->modalConfig['buttonText'] }}
</flux:button>
</div>
<div class="space-y-4">
<div class="relative flex items-center justify-center w-full">
<div class="absolute inset-0 w-full h-px top-1/2 bg-stone-200 dark:bg-stone-600"></div>
<span class="relative px-2 text-sm bg-white dark:bg-stone-800 text-stone-600 dark:text-stone-400">
{{ __('or, enter the code manually') }}
</span>
</div>
<div
class="flex items-center space-x-2"
x-data="{
copied: false,
async copy() {
try {
await navigator.clipboard.writeText('{{ $manualSetupKey }}');
this.copied = true;
setTimeout(() => this.copied = false, 1500);
} catch (e) {
console.warn('Could not copy to clipboard');
}
}
}"
>
<div class="flex items-stretch w-full border rounded-xl dark:border-stone-700">
@empty($manualSetupKey)
<div class="flex items-center justify-center w-full p-3 bg-stone-100 dark:bg-stone-700">
<flux:icon.loading variant="mini"/>
</div>
@else
<input
type="text"
readonly
value="{{ $manualSetupKey }}"
class="w-full p-3 bg-transparent outline-none text-stone-900 dark:text-stone-100"
/>
<button
@click="copy()"
class="px-3 transition-colors border-l cursor-pointer border-stone-200 dark:border-stone-600"
>
<flux:icon.document-duplicate x-show="!copied" variant="outline"></flux:icon>
<flux:icon.check
x-show="copied"
variant="solid"
class="text-green-500"
></flux:icon>
</button>
@endempty
</div>
</div>
</div>
@endif
</div>
</flux:modal>
@endif
@if ($canManagePasskeys)
<section class="mt-12">
<flux:heading>{{ __('Passkeys') }}</flux:heading>
<flux:subheading>{{ __('Manage your passkeys for passwordless sign-in') }}</flux:subheading>
<div class="mt-6 flex flex-col w-full mx-auto space-y-6 text-sm" wire:cloak>
<div class="border rounded-lg border-zinc-200 dark:border-zinc-700 overflow-hidden">
@forelse ($passkeys as $passkey)
<div class="flex items-center justify-between p-4 {{ ! $loop->last ? 'border-b border-zinc-200 dark:border-zinc-700' : '' }}">
<div class="flex items-center gap-4">
<div class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-zinc-100 dark:bg-zinc-800">
<flux:icon.key class="size-5 text-zinc-500 dark:text-zinc-400" />
</div>
<div class="space-y-1">
<div class="flex items-center gap-2.5">
<p class="font-medium tracking-tight">{{ $passkey['name'] }}</p>
@if ($passkey['authenticator'])
<flux:badge size="sm">{{ $passkey['authenticator'] }}</flux:badge>
@endif
</div>
<p class="text-zinc-500 dark:text-zinc-400 text-xs">
{{ __('Added :time', ['time' => $passkey['created_at_diff']]) }}
@if ($passkey['last_used_at_diff'])
<span class="opacity-50 mx-1">/</span>
{{ __('Last used :time', ['time' => $passkey['last_used_at_diff']]) }}
@endif
</p>
</div>
</div>
<flux:button
variant="ghost"
size="sm"
icon="trash"
icon:variant="outline"
wire:click="confirmDelete({{ $passkey['id'] }})"
class="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/50"
/>
</div>
@empty
<div class="p-8 text-center">
<div class="mx-auto mb-4 flex size-14 items-center justify-center rounded-2xl bg-zinc-100 dark:bg-zinc-800">
<flux:icon.key class="size-7 text-zinc-400 dark:text-zinc-500" />
</div>
<p class="font-medium">{{ __('No passkeys yet') }}</p>
<flux:text class="mt-1">{{ __('Add a passkey to sign in without a password') }}</flux:text>
</div>
@endforelse
</div>
<x-passkey-registration />
</div>
</section>
@endif
</x-settings.layout>
<flux:modal
name="delete-passkey-modal"
class="max-w-md md:min-w-md"
@close="closeDeleteModal"
wire:model="showDeleteModal"
>
<div class="space-y-6">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Remove passkey') }}</flux:heading>
<flux:text>
{{ __('Are you sure you want to remove the passkey ":name"? You will no longer be able to use it to sign in.', ['name' => $deletingPasskeyName]) }}
</flux:text>
</div>
<div class="flex gap-3 justify-end">
<flux:button
variant="outline"
wire:click="closeDeleteModal"
>
{{ __('Cancel') }}
</flux:button>
<flux:button
variant="danger"
wire:click="deletePasskey"
>
{{ __('Remove passkey') }}
</flux:button>
</div>
</div>
</flux:modal>
</section>
@@ -0,0 +1,89 @@
<div
class="py-6 space-y-6 border shadow-sm rounded-xl border-zinc-200 dark:border-white/10"
wire:cloak
x-data="{ showRecoveryCodes: false }"
>
<div class="px-6 space-y-2">
<div class="flex items-center gap-2">
<flux:icon.lock-closed variant="outline" class="size-4"/>
<flux:heading size="lg" level="3">{{ __('2FA recovery codes') }}</flux:heading>
</div>
<flux:text variant="subtle">
{{ __('Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.') }}
</flux:text>
</div>
<div class="px-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<flux:button
x-show="!showRecoveryCodes"
icon="eye"
icon:variant="outline"
variant="primary"
@click="showRecoveryCodes = true;"
aria-expanded="false"
aria-controls="recovery-codes-section"
>
{{ __('View recovery codes') }}
</flux:button>
<flux:button
x-show="showRecoveryCodes"
icon="eye-slash"
icon:variant="outline"
variant="primary"
@click="showRecoveryCodes = false"
aria-expanded="true"
aria-controls="recovery-codes-section"
>
{{ __('Hide recovery codes') }}
</flux:button>
@if (filled($recoveryCodes))
<flux:button
x-show="showRecoveryCodes"
icon="arrow-path"
variant="filled"
wire:click="regenerateRecoveryCodes"
>
{{ __('Regenerate codes') }}
</flux:button>
@endif
</div>
<div
x-show="showRecoveryCodes"
x-transition
id="recovery-codes-section"
class="relative overflow-hidden"
x-bind:aria-hidden="!showRecoveryCodes"
>
<div class="mt-3 space-y-3">
@error('recoveryCodes')
<flux:callout variant="danger" icon="x-circle" heading="{{$message}}"/>
@enderror
@if (filled($recoveryCodes))
<div
class="grid gap-1 p-4 font-mono text-sm rounded-lg bg-zinc-100 dark:bg-white/5"
role="list"
aria-label="{{ __('Recovery codes') }}"
>
@foreach($recoveryCodes as $code)
<div
role="listitem"
class="select-text"
wire:loading.class="opacity-50 animate-pulse"
>
{{ $code }}
</div>
@endforeach
</div>
<flux:text variant="subtle" class="text-xs">
{{ __('Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate codes above.') }}
</flux:text>
@endif
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="csrf-token" content="{{ csrf_token() }}"><title>@yield('title','Administration') · Cresta 31</title><link rel="stylesheet" href="{{ asset('css/cresta-admin.css') }}"></head><body class="admin-body"><input type="checkbox" id="nav-toggle" class="nav-toggle"><label for="nav-toggle" class="nav-shade"></label><aside class="sidebar"><a class="brand" href="{{ route('admin.dashboard') }}"><span class="brand-symbol">C<span>31</span></span><span><strong>Cresta 31</strong><small>ADMINISTRATION</small></span></a><div class="side-scroll"><div class="nav-caption">OVERVIEW</div><a class="nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" href="{{ route('admin.dashboard') }}"><span class="ico">▦</span> Dashboard</a><div class="nav-caption">LODGE MANAGEMENT</div><a class="nav-item {{ request()->routeIs('lodge.admin') ? 'active' : '' }}" href="{{ route('lodge.admin') }}"><span class="ico">▤</span> Bookings</a><a class="nav-item {{ request()->routeIs('admin.photos') ? 'active' : '' }}" href="{{ route('admin.photos') }}"><span class="ico">▧</span> Photo gallery</a><a class="nav-item" href="{{ route('home') }}#book" target="_blank" rel="noopener"><span class="ico">▦</span> Availability <span class="external">↗</span></a><div class="nav-caption">SYSTEM</div>@if(\Illuminate\Support\Facades\Route::has('admin.modules.index'))<a class="nav-item {{ request()->routeIs('admin.modules.*') ? 'active' : '' }}" href="{{ route('admin.modules.index') }}"><span class="ico">◈</span> Modules</a>@elseif(\Illuminate\Support\Facades\Route::has('modules.index'))<a class="nav-item" href="{{ route('modules.index') }}"><span class="ico">◈</span> Modules</a>@endif<a class="nav-item" href="{{ route('profile.edit') }}"><span class="ico">⚙</span> Account settings</a><a class="nav-item" href="{{ route('home') }}" target="_blank" rel="noopener"><span class="ico">↗</span> View website</a></div><div class="side-footer"><div class="avatar">{{ strtoupper(substr(auth()->user()->name ?? 'A',0,1)) }}</div><div class="user"><strong>{{ auth()->user()->name }}</strong><small>{{ auth()->user()->email }}</small></div></div></aside><div class="main-shell"><header class="topbar"><label for="nav-toggle" class="menu-toggle" aria-label="Toggle sidebar">☰</label><div class="crumb">Cresta 31 <span>/</span> @yield('section','Dashboard')</div><div class="top-actions"><a class="site-link" href="{{ route('home') }}" target="_blank" rel="noopener">View site ↗</a><form method="POST" action="{{ route('logout') }}">@csrf<button class="logout" type="submit">Sign out</button></form></div></header><main class="main-content">@if(session('success'))<div class="notice success">{{ session('success') }}</div>@endif@if($errors->any())<div class="notice error">{{ $errors->first() }}</div>@endif@yield('content')</main><footer class="admin-footer">Cresta 31 · Administration <span>{{ date('Y') }}</span></footer></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="csrf-token" content="{{ csrf_token() }}"><title>@yield('title', $title ?? 'Administration') · Cresta 31</title>@include('partials.head')<link rel="stylesheet" href="{{ asset('css/cresta-admin.css') }}"><style>.cresta-settings-content{max-width:1100px;margin:0 auto;padding:8px 0 36px}.cresta-settings-content section{max-width:100%}.cresta-settings-content input{max-width:100%}</style></head><body class="admin-body"><input type="checkbox" id="nav-toggle" class="nav-toggle"><label for="nav-toggle" class="nav-shade"></label><aside class="sidebar"><a class="brand" href="{{ route('admin.dashboard') }}"><span class="brand-symbol">C<span>31</span></span><span><strong>Cresta 31</strong><small>ADMINISTRATION</small></span></a><div class="side-scroll">@if(auth()->check() && filled(config('lodge.admin_email')) && strcasecmp(auth()->user()->email, config('lodge.admin_email')) === 0)<div class="nav-caption">OVERVIEW</div><a class="nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" href="{{ route('admin.dashboard') }}"><span class="ico">▦</span> Dashboard</a><div class="nav-caption">LODGE MANAGEMENT</div><a class="nav-item {{ request()->routeIs('lodge.admin') ? 'active' : '' }}" href="{{ route('lodge.admin') }}"><span class="ico">▤</span> Bookings</a><a class="nav-item {{ request()->routeIs('admin.photos') ? 'active' : '' }}" href="{{ route('admin.photos') }}"><span class="ico">▧</span> Photo gallery</a><a class="nav-item" href="{{ route('home') }}#book" target="_blank" rel="noopener"><span class="ico">▦</span> Availability <span class="external">↗</span></a><div class="nav-caption">SYSTEM</div>@if(\Illuminate\Support\Facades\Route::has('admin.modules.index'))<a class="nav-item {{ request()->routeIs('admin.modules.*') ? 'active' : '' }}" href="{{ route('admin.modules.index') }}"><span class="ico">◈</span> Modules</a>@elseif(\Illuminate\Support\Facades\Route::has('modules.index'))<a class="nav-item" href="{{ route('modules.index') }}"><span class="ico">◈</span> Modules</a>@endif<a class="nav-item" href="{{ route('profile.edit') }}"><span class="ico">⚙</span> Account settings</a><a class="nav-item" href="{{ route('home') }}" target="_blank" rel="noopener"><span class="ico">↗</span> View website</a>@else<a class="nav-item" href="{{ route('profile.edit') }}">Account settings</a><a class="nav-item" href="{{ url('/') }}">View website</a>@endif</div><div class="side-footer"><div class="avatar">{{ strtoupper(substr(auth()->user()->name ?? 'A',0,1)) }}</div><div class="user"><strong>{{ auth()->user()->name }}</strong><small>{{ auth()->user()->email }}</small></div></div></aside><div class="main-shell"><header class="topbar"><label for="nav-toggle" class="menu-toggle" aria-label="Toggle sidebar">☰</label><div class="crumb">Cresta 31 <span>/</span> @yield('section','Dashboard')</div><div class="top-actions"><a class="site-link" href="{{ route('home') }}" target="_blank" rel="noopener">View site ↗</a><form method="POST" action="{{ route('logout') }}">@csrf<button class="logout" type="submit">Sign out</button></form></div></header><main class="main-content">@if(session('success'))<div class="notice success">{{ session('success') }}</div>@endif@if($errors->any())<div class="notice error">{{ $errors->first() }}</div>@endif@hasSection('content')@yield('content')@else<div class="cresta-settings-content">{{ $slot ?? '' }}</div>@endif</main><footer class="admin-footer">Cresta 31 · Administration <span>{{ date('Y') }}</span></footer></div>@fluxScripts</body></html>
+5 -1
View File
@@ -1,3 +1,7 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="csrf-token" content="{{ csrf_token() }}"><title>@yield('title','Cresta 31 | Lodge escapes')</title><meta name="description" content="Escape to Cresta 31. Discover the lodge, check availability and request your stay."><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Playfair+Display:wght@500;600&display=swap" rel="stylesheet"><style>
:root{--ink:#1b312b;--muted:#697b70;--sand:#f6f3eb;--gold:#c49b5a;--white:#fff}*{box-sizing:border-box}body{margin:0;background:var(--sand);color:var(--ink);font-family:'DM Sans',sans-serif}a{color:inherit;text-decoration:none}h1,h2,h3{font-family:'Playfair Display',serif;font-weight:500}header{position:absolute;top:0;left:0;right:0;z-index:2;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:25px max(5vw,24px)}header .brand{font-family:'Playfair Display',serif;font-size:30px}header nav{display:flex;gap:25px;align-items:center}header a.button{background:#fff;color:var(--ink)}.button,button{display:inline-block;background:var(--ink);color:white;border:0;padding:15px 24px;border-radius:3px;font:600 14px 'DM Sans';cursor:pointer}.button:hover,button:hover{opacity:.88}.wrap{max-width:1200px;margin:auto;padding:0 24px}.hero{height:min(85vh,780px);min-height:530px;position:relative;display:flex;align-items:center;background:#233d34 url('/images/lodge/hero/photo-01.webp') center/cover;color:#fff}.hero:before{content:'';position:absolute;inset:0;background:linear-gradient(90deg,rgba(12,30,24,.7),rgba(12,30,24,.08))}.hero .content{position:relative;max-width:750px;padding:80px 0}.eyebrow{text-transform:uppercase;letter-spacing:.24em;font-size:12px;font-weight:700}.hero h1{font-size:clamp(50px,7vw,95px);line-height:1.05;margin:20px 0}.hero p{font-size:18px;max-width:480px;line-height:1.8}.section{padding:85px 0}.section h2{font-size:clamp(34px,4vw,54px);margin:10px 0 25px}.intro{display:grid;grid-template-columns:1fr 1fr;gap:70px;align-items:center}.intro p{line-height:1.9;color:var(--muted)}.intro img{width:100%;height:480px;object-fit:cover}.gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.gallery img{width:100%;height:275px;object-fit:cover;border-radius:3px}.booking{background:#fff;padding:34px;box-shadow:0 14px 60px #172e2220;border-radius:5px}.booking-grid{display:grid;grid-template-columns:repeat(3,1fr) auto;gap:15px;align-items:end}label{display:block;font-size:12px;text-transform:uppercase;letter-spacing:.12em;font-weight:700;margin-bottom:9px}input,select,textarea{display:block;width:100%;border:1px solid #d5ddd7;padding:14px;font:inherit;border-radius:3px;background:#fff;color:var(--ink)}.small{color:var(--muted);font-size:14px}.card{background:#fff;padding:36px;border-radius:5px;max-width:760px;margin:auto}.card h1{font-size:46px}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.full{grid-column:1/-1}.errors{background:#fff0ed;color:#8c3024;padding:16px;margin:16px 0}.subhero{background:var(--ink);color:#fff;padding:155px 24px 65px}.subhero h1{font-size:54px;margin:0}footer{background:#142820;color:#fff;padding:45px 0;margin-top:30px}footer .wrap{display:flex;justify-content:space-between}.price{font-size:30px;font-family:'Playfair Display',serif}table{border-collapse:collapse;width:100%;background:white}td,th{padding:13px;text-align:left;border-bottom:1px solid #ddd}@media(max-width:760px){header nav a:not(.button){display:none}.intro{grid-template-columns:1fr;gap:25px}.gallery{grid-template-columns:repeat(2,1fr)}.gallery img{height:180px}.booking-grid,.form-grid{grid-template-columns:1fr}.hero{min-height:600px}.section{padding:55px 0}footer .wrap{display:block}}
</style></head><body><header><a class="brand" href="{{ route('home') }}">Cresta 31<span style="color:#d9b985">.</span></a><nav><a href="{{ route('home') }}#the-lodge">The lodge</a><a href="{{ route('home') }}#gallery">Gallery</a><a class="button" href="{{ route('home') }}#book">Check availability</a>@auth @if(filled(config('lodge.admin_email')) && strcasecmp(trim(auth()->user()->email),trim(config('lodge.admin_email')))===0)<a href="{{ route('admin.dashboard') }}" style="border:1px solid rgba(255,255,255,.8);padding:10px 15px;border-radius:4px;font-weight:700">Admin dashboard →</a>@endif @endauth</nav></header>@yield('content')<footer><div class="wrap"><div><strong>Cresta 31</strong><p class="small">Your next lodge escape starts here.</p></div><div>© {{ date('Y') }} Cresta 31</div></div></footer></body></html>
</style></head><body><header><a class="brand" href="{{ route('home') }}">Cresta 31<span style="color:#d9b985">.</span></a><nav><a href="{{ route('home') }}#the-lodge">The lodge</a><a href="{{ route('home') }}#gallery">Gallery</a><a class="button" href="{{ route('home') }}#book">Check availability</a>@auth
@if(filled(config('lodge.admin_email')) && strcasecmp(trim((string) auth()->user()->email), trim((string) config('lodge.admin_email'))) === 0)
<a href="{{ route('admin.dashboard') }}" class="button" style="background:#d9b985;color:#1b312b">Administration</a>
@endif
@endauth</nav></header>@yield('content')<footer><div class="wrap"><div><strong>Cresta 31</strong><p class="small">Your next lodge escape starts here.</p></div><div>© {{ date('Y') }} Cresta 31</div></div></footer></body></html>
@@ -0,0 +1,5 @@
<div class="relative mb-6 w-full">
<flux:heading size="xl" level="1">{{ __('Settings') }}</flux:heading>
<flux:subheading size="lg" class="mb-6">{{ __('Manage your profile and account settings') }}</flux:subheading>
<flux:separator variant="subtle" />
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ use App\Models\LodgeBooking;
use App\Models\LodgePhoto;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth', 'verified'])->prefix('admin')->group(function () {
Route::middleware(['auth', 'verified', \App\Http\Middleware\EnsureLodgeAdmin::class])->prefix('admin')->group(function () {
$isAdmin = static function ($request): void {
abort_unless(
filled(config('lodge.admin_email'))
+1 -1
View File
@@ -6,7 +6,7 @@ Route::get('/',[LodgeController::class,'home'])->name('home');
Route::get('/availability',[LodgeController::class,'availability'])->name('lodge.availability');
Route::post('/book',[LodgeController::class,'book'])->middleware('throttle:8,1')->name('lodge.book');
Route::get('/booking/{reference}',[LodgeController::class,'confirmation'])->middleware('throttle:30,1')->name('lodge.confirmation');
Route::middleware(['auth','verified'])->group(function(){Route::get('/dashboard',fn()=>redirect()->route('lodge.admin'))->name('dashboard');Route::get('/admin/bookings',[LodgeController::class,'admin'])->name('lodge.admin');Route::patch('/admin/bookings/{booking}',[LodgeController::class,'update'])->name('lodge.update');Route::post('/admin/photos',[LodgePhotoController::class,'store'])->name('lodge.photos.store');Route::delete('/admin/photos/{photo}',[LodgePhotoController::class,'destroy'])->name('lodge.photos.destroy');});
Route::middleware(['auth','verified',\App\Http\Middleware\EnsureLodgeAdmin::class])->group(function(){Route::get('/dashboard',fn()=>redirect()->route('admin.dashboard'))->name('dashboard');Route::get('/admin/bookings',[LodgeController::class,'admin'])->name('lodge.admin');Route::patch('/admin/bookings/{booking}',[LodgeController::class,'update'])->name('lodge.update');Route::post('/admin/photos',[LodgePhotoController::class,'store'])->name('lodge.photos.store');Route::delete('/admin/photos/{photo}',[LodgePhotoController::class,'destroy'])->name('lodge.photos.destroy');});
require __DIR__.'/settings.php';
require __DIR__.'/cresta-admin.php';
@@ -0,0 +1,2 @@
{{-- Use the same Cresta 31 shell for Livewire settings and admin pages. --}}
@include('lodge.admin-layout', ['slot' => $slot, 'title' => $title ?? 'Account settings'])
+2
View File
@@ -0,0 +1,2 @@
{{-- Use the same Cresta 31 shell for Livewire settings and admin pages. --}}
@include('lodge.admin-layout', ['slot' => $slot, 'title' => $title ?? 'Account settings'])
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="csrf-token" content="{{ csrf_token() }}"><title>@yield('title', $title ?? 'Administration') · Cresta 31</title>@include('partials.head')<link rel="stylesheet" href="{{ asset('css/cresta-admin.css') }}"><style>.cresta-settings-content{max-width:1100px;margin:0 auto;padding:8px 0 36px}.cresta-settings-content section{max-width:100%}.cresta-settings-content input{max-width:100%}</style></head><body class="admin-body"><input type="checkbox" id="nav-toggle" class="nav-toggle"><label for="nav-toggle" class="nav-shade"></label><aside class="sidebar"><a class="brand" href="{{ route('admin.dashboard') }}"><span class="brand-symbol">C<span>31</span></span><span><strong>Cresta 31</strong><small>ADMINISTRATION</small></span></a><div class="side-scroll">@if(auth()->check() && filled(config('lodge.admin_email')) && strcasecmp(auth()->user()->email, config('lodge.admin_email')) === 0)<div class="nav-caption">OVERVIEW</div><a class="nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" href="{{ route('admin.dashboard') }}"><span class="ico">▦</span> Dashboard</a><div class="nav-caption">LODGE MANAGEMENT</div><a class="nav-item {{ request()->routeIs('lodge.admin') ? 'active' : '' }}" href="{{ route('lodge.admin') }}"><span class="ico">▤</span> Bookings</a><a class="nav-item {{ request()->routeIs('admin.photos') ? 'active' : '' }}" href="{{ route('admin.photos') }}"><span class="ico">▧</span> Photo gallery</a><a class="nav-item" href="{{ route('home') }}#book" target="_blank" rel="noopener"><span class="ico">▦</span> Availability <span class="external">↗</span></a><div class="nav-caption">SYSTEM</div>@if(\Illuminate\Support\Facades\Route::has('admin.modules.index'))<a class="nav-item {{ request()->routeIs('admin.modules.*') ? 'active' : '' }}" href="{{ route('admin.modules.index') }}"><span class="ico">◈</span> Modules</a>@elseif(\Illuminate\Support\Facades\Route::has('modules.index'))<a class="nav-item" href="{{ route('modules.index') }}"><span class="ico">◈</span> Modules</a>@endif<a class="nav-item" href="{{ route('profile.edit') }}"><span class="ico">⚙</span> Account settings</a><a class="nav-item" href="{{ route('home') }}" target="_blank" rel="noopener"><span class="ico">↗</span> View website</a>@else<a class="nav-item" href="{{ route('profile.edit') }}">Account settings</a><a class="nav-item" href="{{ url('/') }}">View website</a>@endif</div><div class="side-footer"><div class="avatar">{{ strtoupper(substr(auth()->user()->name ?? 'A',0,1)) }}</div><div class="user"><strong>{{ auth()->user()->name }}</strong><small>{{ auth()->user()->email }}</small></div></div></aside><div class="main-shell"><header class="topbar"><label for="nav-toggle" class="menu-toggle" aria-label="Toggle sidebar">☰</label><div class="crumb">Cresta 31 <span>/</span> @yield('section','Dashboard')</div><div class="top-actions"><a class="site-link" href="{{ route('home') }}" target="_blank" rel="noopener">View site ↗</a><form method="POST" action="{{ route('logout') }}">@csrf<button class="logout" type="submit">Sign out</button></form></div></header><main class="main-content">@if(session('success'))<div class="notice success">{{ session('success') }}</div>@endif@if($errors->any())<div class="notice error">{{ $errors->first() }}</div>@endif@hasSection('content')@yield('content')@else<div class="cresta-settings-content">{{ $slot ?? '' }}</div>@endif</main><footer class="admin-footer">Cresta 31 · Administration <span>{{ date('Y') }}</span></footer></div>@fluxScripts</body></html>