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/LODGE_SETUP.md b/LODGE_SETUP.md new file mode 100644 index 0000000..4bb9d85 --- /dev/null +++ b/LODGE_SETUP.md @@ -0,0 +1,22 @@ +# Cresta 31 lodge booking website + +Built on the supplied Laravel 13 / Livewire starter project. Includes the 11 supplied lodge images, public gallery, date search, pricing, guest request form, conflict checking, booking references and a restricted admin booking list. + +## Installation + +1. Configure PHP 8.3+ and Composer, Node.js, and a MySQL or PostgreSQL database. +2. Run `composer install` (the original project depends on a private MPBA Git repository; access is required). +3. Copy `.env.example` to `.env`; set `APP_URL`, `APP_KEY` (with `php artisan key:generate`) and `DB_*` values. +4. Set `LODGE_NIGHTLY_RATE_PENCE=17500`, `LODGE_MAX_GUESTS=6`, and `LODGE_ADMIN_EMAIL=your-admin-email@example.com` in `.env`. +5. Run `php artisan migrate`, `npm ci`, `npm run build`. +6. Create a Laravel user with the same email as `LODGE_ADMIN_EMAIL` via your normal registration process, then sign in and visit `/admin/bookings`. +7. Serve Laravel's `public/` directory with your web server. + +## Important before taking real bookings + +- The £175 nightly rate and 6-guest capacity are **placeholders**. Set actual rates and capacity. +- This build accepts booking **requests** and blocks overlapping pending/confirmed stays; owners confirm them manually. It does **not** process payments, send booking emails, or integrate an external calendar. Do not advertise online payment or automatic confirmation. +- Booking status pages are accessible to anyone with the long random reference; avoid sharing references publicly. +- For production, configure backups, mail notifications, cancellation policy, terms, privacy information, accurate property description, and availability blocks for owner stays. +- Review photographs and add verified property details (address, facilities, check-in/out times, policies). +- The starter archive included an `.env` file, which was intentionally excluded to avoid distributing secrets. diff --git a/app/Http/Controllers/LodgeController.php b/app/Http/Controllers/LodgeController.php new file mode 100644 index 0000000..094930a --- /dev/null +++ b/app/Http/Controllers/LodgeController.php @@ -0,0 +1,34 @@ +$this->nightlyRate()/100]); } + 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); + if($nights>30) throw ValidationException::withMessages(['check_out'=>'Maximum stay is 30 nights.']); + $available=!LodgeBooking::whereIn('status',['pending','confirmed'])->where('check_in','<',$v['check_out'])->where('check_out','>',$v['check_in'])->exists(); + return view('lodge.availability',compact('v','available','nights')+['total'=>$nights*$this->nightlyRate()/100]); + } + public function book(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),'guest_name'=>'required|string|max:120','guest_email'=>'required|email|max:255','guest_phone'=>'nullable|string|max:40','notes'=>'nullable|string|max:2000']); + $nights=CarbonImmutable::parse($v['check_in'])->diffInDays(CarbonImmutable::parse($v['check_out'])); + if($nights>30) throw ValidationException::withMessages(['check_out'=>'Maximum stay is 30 nights.']); + // Serialized booking creation protects against concurrent requests on supported database engines. + $booking=DB::transaction(function()use($v,$nights){ + DB::table('lodge_booking_locks')->where('id',1)->lockForUpdate()->first(); + if(LodgeBooking::whereIn('status',['pending','confirmed'])->where('check_in','<',$v['check_out'])->where('check_out','>',$v['check_in'])->exists()) throw ValidationException::withMessages(['check_in'=>'These dates have just been booked. Please choose different dates.']); + return LodgeBooking::create($v+['reference'=>'CR31-'.strtoupper(Str::random(9)),'nights'=>$nights,'total_pence'=>$nights*$this->nightlyRate(),'status'=>'pending']); + },3); + 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 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/Models/LodgeBooking.php b/app/Models/LodgeBooking.php new file mode 100644 index 0000000..386d457 --- /dev/null +++ b/app/Models/LodgeBooking.php @@ -0,0 +1,7 @@ +'date','check_out'=>'date','total_pence'=>'integer']; } +} diff --git a/composer.json b/composer.json index 0e4fa2b..3b4d72a 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,13 @@ "livewire/blaze": "^1.0", "livewire/flux": "^2.13.1", "livewire/livewire": "^4.1", - "predis/predis": "^3.5", - "mpba/laravel-modules": "dev-main" + "mpba/laravel-modules": "dev-main", + "predis/predis": "^3.7" }, "require-dev": { + "barryvdh/laravel-debugbar": "^4.3", + "barryvdh/laravel-ide-helper": "^3.7", + "beyondcode/laravel-query-detector": "^2.3", "deployer/deployer": "^8.0", "fakerphp/faker": "^1.24", "larastan/larastan": "^3.9", @@ -32,10 +35,7 @@ "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.9.3", "pestphp/pest": "^4.7", - "pestphp/pest-plugin-laravel": "^4.1", - "barryvdh/laravel-debugbar": "^4.3", - "barryvdh/laravel-ide-helper": "^3.7", - "beyondcode/laravel-query-detector": "^2.3" + "pestphp/pest-plugin-laravel": "^4.1" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 7e26f50..8de6ba9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "26f6a407b1691a705ed51437c1714947", + "content-hash": "25ec87a8590381dbc5fe052bdc791780", "packages": [ { "name": "bacon/bacon-qr-code", @@ -3607,16 +3607,16 @@ }, { "name": "predis/predis", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "2ff20c08bb63697245ffee3f198d1673086c302d" + "reference": "4d91f3b08a60c3bacc2afbd8dc41724559b7626a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/2ff20c08bb63697245ffee3f198d1673086c302d", - "reference": "2ff20c08bb63697245ffee3f198d1673086c302d", + "url": "https://api.github.com/repos/predis/predis/zipball/4d91f3b08a60c3bacc2afbd8dc41724559b7626a", + "reference": "4d91f3b08a60c3bacc2afbd8dc41724559b7626a", "shasum": "" }, "require": { @@ -3658,7 +3658,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v3.6.0" + "source": "https://github.com/predis/predis/tree/v3.7.0" }, "funding": [ { @@ -3666,7 +3666,7 @@ "type": "github" } ], - "time": "2026-08-14T23:07:56+00:00" + "time": "2026-10-08T16:46:04+00:00" }, { "name": "psr/clock", diff --git a/config/lodge.php b/config/lodge.php new file mode 100644 index 0000000..dc15686 --- /dev/null +++ b/config/lodge.php @@ -0,0 +1,2 @@ +(int)env('LODGE_NIGHTLY_RATE_PENCE',17500),'max_guests'=>(int)env('LODGE_MAX_GUESTS',6),'admin_email'=>env('LODGE_ADMIN_EMAIL')]; diff --git a/database/migrations/2026_10_15_000001_create_lodge_bookings_table.php b/database/migrations/2026_10_15_000001_create_lodge_bookings_table.php new file mode 100644 index 0000000..558eba4 --- /dev/null +++ b/database/migrations/2026_10_15_000001_create_lodge_bookings_table.php @@ -0,0 +1,10 @@ +id();$t->string('reference')->unique();$t->string('guest_name');$t->string('guest_email');$t->string('guest_phone')->nullable(); + $t->date('check_in')->index();$t->date('check_out')->index();$t->unsignedTinyInteger('guests');$t->unsignedInteger('nights');$t->unsignedInteger('total_pence');$t->string('status')->default('pending')->index();$t->text('notes')->nullable();$t->timestamps(); }); } + public function down(): void { Schema::dropIfExists('lodge_bookings'); } +}; diff --git a/database/migrations/2026_10_15_000002_create_lodge_booking_locks_table.php b/database/migrations/2026_10_15_000002_create_lodge_booking_locks_table.php new file mode 100644 index 0000000..97c6cb8 --- /dev/null +++ b/database/migrations/2026_10_15_000002_create_lodge_booking_locks_table.php @@ -0,0 +1,6 @@ +$t->unsignedTinyInteger('id')->primary());DB::table('lodge_booking_locks')->insert(['id'=>1]);}public function down():void{Schema::dropIfExists('lodge_booking_locks');}}; diff --git a/deploy.php b/deploy.php index a63f706..b7d0631 100644 --- a/deploy.php +++ b/deploy.php @@ -9,7 +9,7 @@ set('writable_use_sudo', true); set('clear_use_sudo', true); set('cleanup_use_sudo', true); set('keep_releases', 1); -set('repository', 'git@absdev.github.com:magabsdev/thredary.git'); +set('repository', 'git@git.absdev.net:mag/cresta31.git'); set('node_version', '22.12.0'); set('allow_anonymous_stats', false); @@ -18,16 +18,12 @@ add('shared_dirs', ['storage']); add('writable_dirs', ['storage', 'bootstrap/cache']); // Host Definitions -host('main') - ->set('hostname', 'thredary.com') + +host('prod') + ->set('hostname', 'cresta31.absdev.net') ->set('remote_user', 'mag') ->set('branch', 'main') - ->set('deploy_path', '/var/www/thredary'); -host('develop') - ->set('hostname', 'thredary.com') - ->set('remote_user', 'mag') - ->set('branch', 'develop') - ->set('deploy_path', '/var/www/thredary_dev'); + ->set('deploy_path', '/var/www/cresta31'); task('build', function (): void { run('export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use {{node_version}} && cd {{release_path}} && npm ci --no-audit --no-fund'); @@ -52,7 +48,7 @@ task('artisan:clear', function (): void { task('services:restart', function (): void { run('sudo -n nginx -t'); run('sudo -n systemctl reload nginx'); - run('sudo -n systemctl restart php8.3-fpm'); + run('sudo -n systemctl restart php8.5-fpm'); }); after('deploy:update_code', 'deploy:write_revision_file'); diff --git a/modules_statuses.json b/modules_statuses.json index 431fdbb..573ab01 100644 --- a/modules_statuses.json +++ b/modules_statuses.json @@ -1 +1,5 @@ -{"Stylist":true} +{ + "Stylist": true, + "M3": true, + "OTM": true +} \ No newline at end of file diff --git a/public/images/lodge/photo-01.png b/public/images/lodge/photo-01.png new file mode 100644 index 0000000..bf92d01 Binary files /dev/null and b/public/images/lodge/photo-01.png differ diff --git a/public/images/lodge/photo-02.png b/public/images/lodge/photo-02.png new file mode 100644 index 0000000..70299cc Binary files /dev/null and b/public/images/lodge/photo-02.png differ diff --git a/public/images/lodge/photo-03.png b/public/images/lodge/photo-03.png new file mode 100644 index 0000000..7cd5b2f Binary files /dev/null and b/public/images/lodge/photo-03.png differ diff --git a/public/images/lodge/photo-04.png b/public/images/lodge/photo-04.png new file mode 100644 index 0000000..3cb22dc Binary files /dev/null and b/public/images/lodge/photo-04.png differ diff --git a/public/images/lodge/photo-05.png b/public/images/lodge/photo-05.png new file mode 100644 index 0000000..f6a519d Binary files /dev/null and b/public/images/lodge/photo-05.png differ diff --git a/public/images/lodge/photo-06.png b/public/images/lodge/photo-06.png new file mode 100644 index 0000000..b475ebe Binary files /dev/null and b/public/images/lodge/photo-06.png differ diff --git a/public/images/lodge/photo-07.png b/public/images/lodge/photo-07.png new file mode 100644 index 0000000..40b3007 Binary files /dev/null and b/public/images/lodge/photo-07.png differ diff --git a/public/images/lodge/photo-08.png b/public/images/lodge/photo-08.png new file mode 100644 index 0000000..9bebc2c Binary files /dev/null and b/public/images/lodge/photo-08.png differ diff --git a/public/images/lodge/photo-09.png b/public/images/lodge/photo-09.png new file mode 100644 index 0000000..d21cf17 Binary files /dev/null and b/public/images/lodge/photo-09.png differ diff --git a/public/images/lodge/photo-10.png b/public/images/lodge/photo-10.png new file mode 100644 index 0000000..2814840 Binary files /dev/null and b/public/images/lodge/photo-10.png differ diff --git a/public/images/lodge/photo-11.png b/public/images/lodge/photo-11.png new file mode 100644 index 0000000..8dc7db7 Binary files /dev/null and b/public/images/lodge/photo-11.png differ diff --git a/resources/themes/frontend/views/lodge/admin.blade.php b/resources/themes/frontend/views/lodge/admin.blade.php new file mode 100644 index 0000000..24dcd11 --- /dev/null +++ b/resources/themes/frontend/views/lodge/admin.blade.php @@ -0,0 +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. | ||||||
{{ $v['check_in'] }} → {{ $v['check_out'] }} · {{ $nights }} nights · {{ $v['guests'] }} guests
@if($available)£{{ number_format($total,2) }} estimated total
Submit a booking request. Your booking remains pending until confirmed by the lodge owner. No payment is taken online.
@if($errors->any())Someone has already requested or booked part of this stay. Please select alternative dates.
Choose new dates@endifYour reference is {{ $booking->reference }}.
{{ $booking->check_in->format('j M Y') }} – {{ $booking->check_out->format('j M Y') }} · {{ $booking->nights }} nights
£{{ number_format($booking->total_pence/100,2) }}
Status: {{ ucfirst($booking->status) }}
This is a request, not a confirmed reservation. No payment has been collected. Please retain your reference.
Back to the lodgeWelcome 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. | ||||||
{{ $v['check_in'] }} → {{ $v['check_out'] }} · {{ $nights }} nights · {{ $v['guests'] }} guests
@if($available)£{{ number_format($total,2) }} estimated total
Submit a booking request. Your booking remains pending until confirmed by the lodge owner. No payment is taken online.
@if($errors->any())Someone has already requested or booked part of this stay. Please select alternative dates.
Choose new dates@endifYour reference is {{ $booking->reference }}.
{{ $booking->check_in->format('j M Y') }} – {{ $booking->check_out->format('j M Y') }} · {{ $booking->nights }} nights
£{{ number_format($booking->total_pence/100,2) }}
Status: {{ ucfirst($booking->status) }}
This is a request, not a confirmed reservation. No payment has been collected. Please retain your reference.
Back to the lodgeWelcome 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.
 }})