A little escape.
A lot to love.
Welcome to Cresta 31 — a comfortable lodge retreat made for slow mornings, fresh air and memorable stays.
Explore your stay →diff --git a/IMAGE_SETUP.md b/IMAGE_SETUP.md new file mode 100644 index 0000000..fa77b8d --- /dev/null +++ b/IMAGE_SETUP.md @@ -0,0 +1,13 @@ +# Cresta 31 automatic image optimisation + +The original 11 PNG photos remain in the repository, but the public-facing homepage uses optimised WebP versions in `public/images/lodge/{hero,gallery,thumb}`. The hero is 1920px maximum, gallery images 1200px, and thumbnails 480px. + +## New uploads + +1. Install GD with WebP support on the production host: `sudo apt install php8.5-gd && sudo systemctl restart php8.5-fpm`. +2. Deploy these changes, then run `cd /var/www/cresta31/current && php artisan migrate --force`. +3. Verify `php artisan storage:link` and that `public/storage` resolves to `shared/storage/app/public` through Deployer's shared storage symlink. +4. Sign in using the account matching `LODGE_ADMIN_EMAIL` in `.env`; open `/admin/bookings` and upload images under **Lodge photographs**. +5. New uploads automatically generate 1600px gallery and 480px thumbnail WebP images in the shared `storage/app/public/lodge` directory, and appear on the public homepage. They persist across Deployer releases. + +Requirements: PHP GD WebP support, writable shared storage, and configured database. The 15 MB per-file and 10-files-per-request limits are application limits; `upload_max_filesize` and `post_max_size` in PHP-FPM must be high enough (e.g. 16M and 160M) for maximum-size batches. The initial 11 static photographs are not managed by the upload UI. diff --git a/app/Http/Controllers/LodgeController.php b/app/Http/Controllers/LodgeController.php index 094930a..1baa535 100644 --- a/app/Http/Controllers/LodgeController.php +++ b/app/Http/Controllers/LodgeController.php @@ -1,6 +1,7 @@ $this->nightlyRate()/100]); } + public function home() { return view('lodge.home',['rate'=>$this->nightlyRate()/100,'uploadedPhotos'=>LodgePhoto::latest()->get()]); } public function availability(Request $request) { $v=$request->validate(['check_in'=>'required|date|after_or_equal:today','check_out'=>'required|date|after:check_in','guests'=>'required|integer|min:1|max:'.config('lodge.max_guests',6)]); $start=CarbonImmutable::parse($v['check_in']);$end=CarbonImmutable::parse($v['check_out']);$nights=$start->diffInDays($end); @@ -29,6 +30,6 @@ class LodgeController extends Controller { return redirect()->route('lodge.confirmation',['reference'=>$booking->reference]); } public function confirmation(string $reference) { $booking=LodgeBooking::where('reference',$reference)->firstOrFail();return view('lodge.confirmation',compact('booking')); } - public function admin(Request $request) { abort_unless($request->user()?->email===config('lodge.admin_email') && filled(config('lodge.admin_email')),403);return view('lodge.admin',['bookings'=>LodgeBooking::latest()->paginate(30)]); } + public function admin(Request $request) { abort_unless($request->user()?->email===config('lodge.admin_email') && filled(config('lodge.admin_email')),403);return view('lodge.admin',['bookings'=>LodgeBooking::latest()->paginate(30),'photos'=>LodgePhoto::latest()->get()]); } public function update(Request $request,LodgeBooking $booking) {abort_unless($request->user()?->email===config('lodge.admin_email') && filled(config('lodge.admin_email')),403);$v=$request->validate(['status'=>'required|in:pending,confirmed,cancelled']);if($v['status']!=='cancelled' && LodgeBooking::whereKeyNot($booking->id)->whereIn('status',['pending','confirmed'])->where('check_in','<',$booking->check_out)->where('check_out','>',$booking->check_in)->exists()) throw ValidationException::withMessages(['status'=>'Date conflict.']);$booking->update($v);return back()->with('success','Booking updated.'); } } diff --git a/app/Http/Controllers/LodgePhotoController.php b/app/Http/Controllers/LodgePhotoController.php new file mode 100644 index 0000000..22da9f8 --- /dev/null +++ b/app/Http/Controllers/LodgePhotoController.php @@ -0,0 +1,89 @@ +user()?->email === config('lodge.admin_email'), 403); + } + + public function store(Request $request) + { + $this->authorizeAdmin($request); + $request->validate([ + 'photos' => ['required','array','min:1','max:10'], + 'photos.*' => ['required','image','mimes:jpeg,png,webp','max:15360'], + ]); + if (!extension_loaded('gd') || !function_exists('imagewebp')) { + throw ValidationException::withMessages(['photos' => 'PHP GD with WebP support is required. Install php8.5-gd.']); + } + foreach ($request->file('photos') as $file) { + $raw = file_get_contents($file->getRealPath()); + $source = @imagecreatefromstring($raw); + unset($raw); + if (!$source) { + throw ValidationException::withMessages(['photos' => 'One image could not be decoded.']); + } + $width = imagesx($source); $height = imagesy($source); + if ($width < 1 || $height < 1 || $width * $height > 45000000) { + imagedestroy($source); + throw ValidationException::withMessages(['photos' => 'Image dimensions are not supported (maximum 45 megapixels).']); + } + $id = (string) Str::uuid(); + $gallery = "lodge/gallery/{$id}.webp"; + $thumbnail = "lodge/thumb/{$id}.webp"; + try { + $this->saveWebp($source, $width, $height, 1600, 82, $gallery); + $this->saveWebp($source, $width, $height, 480, 77, $thumbnail); + LodgePhoto::create(['gallery_path'=>$gallery,'thumbnail_path'=>$thumbnail,'alt_text'=>'Cresta 31 lodge photograph']); + } catch (\Throwable $e) { + Storage::disk('public')->delete([$gallery,$thumbnail]); + throw $e; + } finally { + imagedestroy($source); + } + } + return back()->with('success', 'Photographs uploaded and optimised.'); + } + + private function saveWebp($source, int $width, int $height, int $maxWidth, int $quality, string $path): void + { + $scale = min(1, $maxWidth / $width); + $w = max(1, (int) round($width * $scale)); + $h = max(1, (int) round($height * $scale)); + $target = imagecreatetruecolor($w, $h); + imagefill($target, 0, 0, imagecolorallocate($target, 255, 255, 255)); + imagecopyresampled($target, $source, 0, 0, 0, 0, $w, $h, $width, $height); + ob_start(); + try { + if (!imagewebp($target, null, $quality)) { + throw new \RuntimeException('WebP conversion failed.'); + } + $bytes = ob_get_clean(); + } catch (\Throwable $e) { + ob_end_clean(); + imagedestroy($target); + throw $e; + } + imagedestroy($target); + if (!Storage::disk('public')->put($path, $bytes)) { + throw new \RuntimeException('Unable to save the optimised image.'); + } + } + + public function destroy(Request $request, LodgePhoto $photo) + { + $this->authorizeAdmin($request); + $paths = [$photo->gallery_path, $photo->thumbnail_path]; + $photo->delete(); + Storage::disk('public')->delete($paths); + return back()->with('success', 'Photograph deleted.'); + } +} diff --git a/app/Models/LodgePhoto.php b/app/Models/LodgePhoto.php new file mode 100644 index 0000000..98c4b19 --- /dev/null +++ b/app/Models/LodgePhoto.php @@ -0,0 +1,6 @@ +id();$table->string('gallery_path');$table->string('thumbnail_path');$table->string('alt_text')->default('Cresta 31 lodge photograph');$table->timestamps();}); } + public function down(): void { Schema::dropIfExists('lodge_photos'); } +}; diff --git a/public/images/lodge/gallery/photo-01.webp b/public/images/lodge/gallery/photo-01.webp new file mode 100644 index 0000000..535bc74 Binary files /dev/null and b/public/images/lodge/gallery/photo-01.webp differ diff --git a/public/images/lodge/gallery/photo-02.webp b/public/images/lodge/gallery/photo-02.webp new file mode 100644 index 0000000..59d7d86 Binary files /dev/null and b/public/images/lodge/gallery/photo-02.webp differ diff --git a/public/images/lodge/gallery/photo-03.webp b/public/images/lodge/gallery/photo-03.webp new file mode 100644 index 0000000..85e666b Binary files /dev/null and b/public/images/lodge/gallery/photo-03.webp differ diff --git a/public/images/lodge/gallery/photo-04.webp b/public/images/lodge/gallery/photo-04.webp new file mode 100644 index 0000000..7f2f7ae Binary files /dev/null and b/public/images/lodge/gallery/photo-04.webp differ diff --git a/public/images/lodge/gallery/photo-05.webp b/public/images/lodge/gallery/photo-05.webp new file mode 100644 index 0000000..28f5b88 Binary files /dev/null and b/public/images/lodge/gallery/photo-05.webp differ diff --git a/public/images/lodge/gallery/photo-06.webp b/public/images/lodge/gallery/photo-06.webp new file mode 100644 index 0000000..358e310 Binary files /dev/null and b/public/images/lodge/gallery/photo-06.webp differ diff --git a/public/images/lodge/gallery/photo-07.webp b/public/images/lodge/gallery/photo-07.webp new file mode 100644 index 0000000..07d8d2f Binary files /dev/null and b/public/images/lodge/gallery/photo-07.webp differ diff --git a/public/images/lodge/gallery/photo-08.webp b/public/images/lodge/gallery/photo-08.webp new file mode 100644 index 0000000..5a80635 Binary files /dev/null and b/public/images/lodge/gallery/photo-08.webp differ diff --git a/public/images/lodge/gallery/photo-09.webp b/public/images/lodge/gallery/photo-09.webp new file mode 100644 index 0000000..ce608cd Binary files /dev/null and b/public/images/lodge/gallery/photo-09.webp differ diff --git a/public/images/lodge/gallery/photo-10.webp b/public/images/lodge/gallery/photo-10.webp new file mode 100644 index 0000000..01577e9 Binary files /dev/null and b/public/images/lodge/gallery/photo-10.webp differ diff --git a/public/images/lodge/gallery/photo-11.webp b/public/images/lodge/gallery/photo-11.webp new file mode 100644 index 0000000..7a7d703 Binary files /dev/null and b/public/images/lodge/gallery/photo-11.webp differ diff --git a/public/images/lodge/hero/photo-01.webp b/public/images/lodge/hero/photo-01.webp new file mode 100644 index 0000000..ff7828c Binary files /dev/null and b/public/images/lodge/hero/photo-01.webp differ diff --git a/public/images/lodge/hero/photo-02.webp b/public/images/lodge/hero/photo-02.webp new file mode 100644 index 0000000..adb898e Binary files /dev/null and b/public/images/lodge/hero/photo-02.webp differ diff --git a/public/images/lodge/hero/photo-03.webp b/public/images/lodge/hero/photo-03.webp new file mode 100644 index 0000000..071e6f4 Binary files /dev/null and b/public/images/lodge/hero/photo-03.webp differ diff --git a/public/images/lodge/hero/photo-04.webp b/public/images/lodge/hero/photo-04.webp new file mode 100644 index 0000000..7bee7ae Binary files /dev/null and b/public/images/lodge/hero/photo-04.webp differ diff --git a/public/images/lodge/hero/photo-05.webp b/public/images/lodge/hero/photo-05.webp new file mode 100644 index 0000000..4357a78 Binary files /dev/null and b/public/images/lodge/hero/photo-05.webp differ diff --git a/public/images/lodge/hero/photo-06.webp b/public/images/lodge/hero/photo-06.webp new file mode 100644 index 0000000..f05502c Binary files /dev/null and b/public/images/lodge/hero/photo-06.webp differ diff --git a/public/images/lodge/hero/photo-07.webp b/public/images/lodge/hero/photo-07.webp new file mode 100644 index 0000000..2cd93f0 Binary files /dev/null and b/public/images/lodge/hero/photo-07.webp differ diff --git a/public/images/lodge/hero/photo-08.webp b/public/images/lodge/hero/photo-08.webp new file mode 100644 index 0000000..02469ab Binary files /dev/null and b/public/images/lodge/hero/photo-08.webp differ diff --git a/public/images/lodge/hero/photo-09.webp b/public/images/lodge/hero/photo-09.webp new file mode 100644 index 0000000..a692615 Binary files /dev/null and b/public/images/lodge/hero/photo-09.webp differ diff --git a/public/images/lodge/hero/photo-10.webp b/public/images/lodge/hero/photo-10.webp new file mode 100644 index 0000000..8caaaa3 Binary files /dev/null and b/public/images/lodge/hero/photo-10.webp differ diff --git a/public/images/lodge/hero/photo-11.webp b/public/images/lodge/hero/photo-11.webp new file mode 100644 index 0000000..1be7da8 Binary files /dev/null and b/public/images/lodge/hero/photo-11.webp differ diff --git a/public/images/lodge/thumb/photo-01.webp b/public/images/lodge/thumb/photo-01.webp new file mode 100644 index 0000000..b83e04e Binary files /dev/null and b/public/images/lodge/thumb/photo-01.webp differ diff --git a/public/images/lodge/thumb/photo-02.webp b/public/images/lodge/thumb/photo-02.webp new file mode 100644 index 0000000..c0d49ca Binary files /dev/null and b/public/images/lodge/thumb/photo-02.webp differ diff --git a/public/images/lodge/thumb/photo-03.webp b/public/images/lodge/thumb/photo-03.webp new file mode 100644 index 0000000..878128e Binary files /dev/null and b/public/images/lodge/thumb/photo-03.webp differ diff --git a/public/images/lodge/thumb/photo-04.webp b/public/images/lodge/thumb/photo-04.webp new file mode 100644 index 0000000..72baebd Binary files /dev/null and b/public/images/lodge/thumb/photo-04.webp differ diff --git a/public/images/lodge/thumb/photo-05.webp b/public/images/lodge/thumb/photo-05.webp new file mode 100644 index 0000000..8573f97 Binary files /dev/null and b/public/images/lodge/thumb/photo-05.webp differ diff --git a/public/images/lodge/thumb/photo-06.webp b/public/images/lodge/thumb/photo-06.webp new file mode 100644 index 0000000..398acb0 Binary files /dev/null and b/public/images/lodge/thumb/photo-06.webp differ diff --git a/public/images/lodge/thumb/photo-07.webp b/public/images/lodge/thumb/photo-07.webp new file mode 100644 index 0000000..9792f1d Binary files /dev/null and b/public/images/lodge/thumb/photo-07.webp differ diff --git a/public/images/lodge/thumb/photo-08.webp b/public/images/lodge/thumb/photo-08.webp new file mode 100644 index 0000000..e19f049 Binary files /dev/null and b/public/images/lodge/thumb/photo-08.webp differ diff --git a/public/images/lodge/thumb/photo-09.webp b/public/images/lodge/thumb/photo-09.webp new file mode 100644 index 0000000..585eb0f Binary files /dev/null and b/public/images/lodge/thumb/photo-09.webp differ diff --git a/public/images/lodge/thumb/photo-10.webp b/public/images/lodge/thumb/photo-10.webp new file mode 100644 index 0000000..3add035 Binary files /dev/null and b/public/images/lodge/thumb/photo-10.webp differ diff --git a/public/images/lodge/thumb/photo-11.webp b/public/images/lodge/thumb/photo-11.webp new file mode 100644 index 0000000..9078bd2 Binary files /dev/null and b/public/images/lodge/thumb/photo-11.webp differ diff --git a/resources/themes/frontend/views/lodge/admin.blade.php b/resources/themes/frontend/views/lodge/admin.blade.php index 24dcd11..87743d8 100644 --- a/resources/themes/frontend/views/lodge/admin.blade.php +++ b/resources/themes/frontend/views/lodge/admin.blade.php @@ -1 +1 @@ -@extends('lodge.layout')@section('title','Manage bookings | Cresta 31')@section('content')
{{ session('success') }}
@endif@if($errors->any())| Reference | Guest | Dates | Guests | Total | Status | Action |
|---|---|---|---|---|---|---|
| {{ $b->reference }} | {{ $b->guest_name }} {{ $b->guest_email }} {{ $b->guest_phone }} | {{ $b->check_in->format('d M Y') }} – {{ $b->check_out->format('d M Y') }} | {{ $b->guests }} | £{{ number_format($b->total_pence/100,2) }} | {{ ucfirst($b->status) }} | |
| No bookings yet. | ||||||
{{ session('success') }}
@endif@if($errors->any())| Reference | Guest | Dates | Guests | Total | Status | Action |
|---|---|---|---|---|---|---|
| {{ $b->reference }} | {{ $b->guest_name }} {{ $b->guest_email }} {{ $b->guest_phone }} | {{ $b->check_in->format('d M Y') }} – {{ $b->check_out->format('d M Y') }} | {{ $b->guests }} | £{{ number_format($b->total_pence/100,2) }} | {{ ucfirst($b->status) }} | |
| No bookings yet. | ||||||
Upload JPEG, PNG or WebP photographs. Images are automatically resized and converted to WebP, and remain available after deployments.
Welcome to Cresta 31 — a comfortable lodge retreat made for slow mornings, fresh air and memorable stays.
Explore your stay →Discover the spaces that make Cresta 31 a special place to get away. Whether you're taking a peaceful break or spending time together, settle in and enjoy a change of scenery.
Browse the lodge photographs below, then choose your dates to check availability.
 }})
Welcome to Cresta 31 — a comfortable lodge retreat made for slow mornings, fresh air and memorable stays.
Explore your stay →Discover the spaces that make Cresta 31 a special place to get away. Whether you're taking a peaceful break or spending time together, settle in and enjoy a change of scenery.
Browse the lodge photographs below, then choose your dates to check availability.
 }})
{{ session('success') }}
@endif@if($errors->any())| Reference | Guest | Dates | Guests | Total | Status | Action |
|---|---|---|---|---|---|---|
| {{ $b->reference }} | {{ $b->guest_name }} {{ $b->guest_email }} {{ $b->guest_phone }} | {{ $b->check_in->format('d M Y') }} – {{ $b->check_out->format('d M Y') }} | {{ $b->guests }} | £{{ number_format($b->total_pence/100,2) }} | {{ ucfirst($b->status) }} | |
| No bookings yet. | ||||||
{{ session('success') }}
@endif@if($errors->any())| Reference | Guest | Dates | Guests | Total | Status | Action |
|---|---|---|---|---|---|---|
| {{ $b->reference }} | {{ $b->guest_name }} {{ $b->guest_email }} {{ $b->guest_phone }} | {{ $b->check_in->format('d M Y') }} – {{ $b->check_out->format('d M Y') }} | {{ $b->guests }} | £{{ number_format($b->total_pence/100,2) }} | {{ ucfirst($b->status) }} | |
| No bookings yet. | ||||||
Upload JPEG, PNG or WebP photographs. Images are automatically resized and converted to WebP, and remain available after deployments.
Welcome to Cresta 31 — a comfortable lodge retreat made for slow mornings, fresh air and memorable stays.
Explore your stay →Discover the spaces that make Cresta 31 a special place to get away. Whether you're taking a peaceful break or spending time together, settle in and enjoy a change of scenery.
Browse the lodge photographs below, then choose your dates to check availability.
 }})
Welcome to Cresta 31 — a comfortable lodge retreat made for slow mornings, fresh air and memorable stays.
Explore your stay →Discover the spaces that make Cresta 31 a special place to get away. Whether you're taking a peaceful break or spending time together, settle in and enjoy a change of scenery.
Browse the lodge photographs below, then choose your dates to check availability.
 }})