@@ -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.
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers;
|
||||
use App\Models\LodgeBooking;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
class LodgeController extends Controller {
|
||||
private function nightlyRate(): int { return max(1,(int)config('lodge.nightly_rate_pence',17500)); }
|
||||
public function home() { return view('lodge.home',['rate'=>$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.'); }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
class LodgeBooking extends Model {
|
||||
protected $fillable=['reference','guest_name','guest_email','guest_phone','check_in','check_out','guests','nights','total_pence','status','notes'];
|
||||
protected function casts(): array { return ['check_in'=>'date','check_out'=>'date','total_pence'=>'integer']; }
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
return ['nightly_rate_pence'=>(int)env('LODGE_NIGHTLY_RATE_PENCE',17500),'max_guests'=>(int)env('LODGE_MAX_GUESTS',6),'admin_email'=>env('LODGE_ADMIN_EMAIL')];
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration {
|
||||
public function up(): void { Schema::create('lodge_bookings', function(Blueprint $t){
|
||||
$t->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'); }
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
return new class extends Migration {public function up(): void {Schema::create('lodge_booking_locks',fn(Blueprint $t)=>$t->unsignedTinyInteger('id')->primary());DB::table('lodge_booking_locks')->insert(['id'=>1]);}public function down():void{Schema::dropIfExists('lodge_booking_locks');}};
|
||||
@@ -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');
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
{"Stylist":true}
|
||||
{
|
||||
"Stylist": true,
|
||||
"M3": true,
|
||||
"OTM": true
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.3 MiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
After Width: | Height: | Size: 6.0 MiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 5.5 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
@@ -0,0 +1 @@
|
||||
@extends('lodge.layout')@section('title','Manage bookings | Cresta 31')@section('content')<div class="subhero"><div class="wrap"><h1>Manage bookings</h1></div></div><main class="section wrap">@if(session('success'))<p>{{ session('success') }}</p>@endif@if($errors->any())<div class="errors">{{ $errors->first() }}</div>@endif<div style="overflow-x:auto"><table><thead><tr><th>Reference</th><th>Guest</th><th>Dates</th><th>Guests</th><th>Total</th><th>Status</th><th>Action</th></tr></thead><tbody>@forelse($bookings as $b)<tr><td>{{ $b->reference }}</td><td>{{ $b->guest_name }}<br><small>{{ $b->guest_email }}<br>{{ $b->guest_phone }}</small></td><td>{{ $b->check_in->format('d M Y') }} – {{ $b->check_out->format('d M Y') }}</td><td>{{ $b->guests }}</td><td>£{{ number_format($b->total_pence/100,2) }}</td><td>{{ ucfirst($b->status) }}</td><td><form method="post" action="{{ route('lodge.update',$b) }}">@csrf @method('PATCH')<select name="status"><option value="pending" @selected($b->status==='pending')>Pending</option><option value="confirmed" @selected($b->status==='confirmed')>Confirmed</option><option value="cancelled" @selected($b->status==='cancelled')>Cancelled</option></select><button style="margin-top:8px">Save</button></form></td></tr>@empty<tr><td colspan="7">No bookings yet.</td></tr>@endforelse</tbody></table></div><div style="margin-top:25px">{{ $bookings->links() }}</div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('lodge.layout')@section('title','Availability | Cresta 31')@section('content')<div class="subhero"><div class="wrap"><h1>Your stay</h1></div></div><main class="section wrap"><div class="card"><h2>{{ $available?'Your dates are available':'These dates are unavailable' }}</h2><p>{{ $v['check_in'] }} → {{ $v['check_out'] }} · {{ $nights }} nights · {{ $v['guests'] }} guests</p>@if($available)<p class="price">£{{ number_format($total,2) }} <span class="small">estimated total</span></p><p class="small">Submit a booking request. Your booking remains pending until confirmed by the lodge owner. No payment is taken online.</p>@if($errors->any())<div class="errors">{{ $errors->first() }}</div>@endif<form method="post" action="{{ route('lodge.book') }}">@csrf<input type="hidden" name="check_in" value="{{ $v['check_in'] }}"><input type="hidden" name="check_out" value="{{ $v['check_out'] }}"><input type="hidden" name="guests" value="{{ $v['guests'] }}"><div class="form-grid"><div><label>Name</label><input name="guest_name" required value="{{ old('guest_name') }}"></div><div><label>Email</label><input name="guest_email" type="email" required value="{{ old('guest_email') }}"></div><div class="full"><label>Phone (optional)</label><input name="guest_phone" value="{{ old('guest_phone') }}"></div><div class="full"><label>Anything we should know? (optional)</label><textarea name="notes" rows="4">{{ old('notes') }}</textarea></div><div class="full"><button type="submit">Request this stay →</button></div></div></form>@else<p>Someone has already requested or booked part of this stay. Please select alternative dates.</p><a class="button" href="{{ route('home') }}#book">Choose new dates</a>@endif</div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('lodge.layout')@section('title','Booking request received | Cresta 31')@section('content')<div class="subhero"><div class="wrap"><h1>Request received</h1></div></div><main class="section wrap"><div class="card"><div class="eyebrow">Cresta 31</div><h2>Thank you for your booking request.</h2><p>Your reference is <strong>{{ $booking->reference }}</strong>.</p><p>{{ $booking->check_in->format('j M Y') }} – {{ $booking->check_out->format('j M Y') }} · {{ $booking->nights }} nights</p><p class="price">£{{ number_format($booking->total_pence/100,2) }}</p><p><strong>Status:</strong> {{ ucfirst($booking->status) }}</p><p class="small">This is a request, not a confirmed reservation. No payment has been collected. Please retain your reference.</p><a class="button" href="{{ route('home') }}">Back to the lodge</a></div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('lodge.layout')@section('content')<section class="hero"><div class="wrap" style="width:100%"><div class="content"><div class="eyebrow">Unwind · Escape · Explore</div><h1>A little escape.<br>A lot to love.</h1><p>Welcome to Cresta 31 — a comfortable lodge retreat made for slow mornings, fresh air and memorable stays.</p><a class="button" style="background:#fff;color:#1b312b" href="#book">Explore your stay →</a></div></div></section><div class="wrap"><div class="booking" id="book" style="position:relative;margin-top:-45px"><form action="{{ route('lodge.availability') }}" method="get"><div class="booking-grid"><div><label for="check_in">Check in</label><input id="check_in" name="check_in" type="date" min="{{ date('Y-m-d') }}" required></div><div><label for="check_out">Check out</label><input id="check_out" name="check_out" type="date" min="{{ date('Y-m-d',strtotime('+1 day')) }}" required></div><div><label for="guests">Guests</label><select id="guests" name="guests">@for($i=1;$i<=config('lodge.max_guests');$i++)<option value="{{ $i }}">{{ $i }} {{ $i===1?'guest':'guests' }}</option>@endfor</select></div><button>Check dates →</button></div></form></div></div><section class="section" id="the-lodge"><div class="wrap intro"><div><div class="eyebrow" style="color:#b48d51">Welcome to Cresta 31</div><h2>Make yourself<br>right at home.</h2><p>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.</p><p>Browse the lodge photographs below, then choose your dates to check availability.</p><div class="price">From £{{ number_format($rate,0) }} <span class="small">/ night</span></div></div><img src="{{ asset('images/lodge/photo-02.png') }}" alt="Cresta 31 lodge interior" loading="lazy"></div></section><section class="section" id="gallery" style="background:#eaece6"><div class="wrap"><div class="eyebrow" style="color:#b48d51">A closer look</div><h2>Explore the lodge</h2><div class="gallery">@for($i=1;$i<=11;$i++)<a href="{{ asset(sprintf('images/lodge/photo-%02d.png',$i)) }}" target="_blank"><img src="{{ asset(sprintf('images/lodge/photo-%02d.png',$i)) }}" loading="lazy" alt="Cresta 31 lodge photo {{ $i }}"></a>@endfor</div></div></section><section class="section"><div class="wrap" style="text-align:center"><div class="eyebrow">Your next getaway</div><h2>Ready to stay?</h2><p class="small">Choose your dates and send a booking request.</p><a class="button" href="#book">Check availability →</a></div></section>@endsection
|
||||
@@ -0,0 +1,3 @@
|
||||
<!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/photo-01.png') 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></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 @@
|
||||
@extends('lodge.layout')@section('title','Manage bookings | Cresta 31')@section('content')<div class="subhero"><div class="wrap"><h1>Manage bookings</h1></div></div><main class="section wrap">@if(session('success'))<p>{{ session('success') }}</p>@endif@if($errors->any())<div class="errors">{{ $errors->first() }}</div>@endif<div style="overflow-x:auto"><table><thead><tr><th>Reference</th><th>Guest</th><th>Dates</th><th>Guests</th><th>Total</th><th>Status</th><th>Action</th></tr></thead><tbody>@forelse($bookings as $b)<tr><td>{{ $b->reference }}</td><td>{{ $b->guest_name }}<br><small>{{ $b->guest_email }}<br>{{ $b->guest_phone }}</small></td><td>{{ $b->check_in->format('d M Y') }} – {{ $b->check_out->format('d M Y') }}</td><td>{{ $b->guests }}</td><td>£{{ number_format($b->total_pence/100,2) }}</td><td>{{ ucfirst($b->status) }}</td><td><form method="post" action="{{ route('lodge.update',$b) }}">@csrf @method('PATCH')<select name="status"><option value="pending" @selected($b->status==='pending')>Pending</option><option value="confirmed" @selected($b->status==='confirmed')>Confirmed</option><option value="cancelled" @selected($b->status==='cancelled')>Cancelled</option></select><button style="margin-top:8px">Save</button></form></td></tr>@empty<tr><td colspan="7">No bookings yet.</td></tr>@endforelse</tbody></table></div><div style="margin-top:25px">{{ $bookings->links() }}</div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('lodge.layout')@section('title','Availability | Cresta 31')@section('content')<div class="subhero"><div class="wrap"><h1>Your stay</h1></div></div><main class="section wrap"><div class="card"><h2>{{ $available?'Your dates are available':'These dates are unavailable' }}</h2><p>{{ $v['check_in'] }} → {{ $v['check_out'] }} · {{ $nights }} nights · {{ $v['guests'] }} guests</p>@if($available)<p class="price">£{{ number_format($total,2) }} <span class="small">estimated total</span></p><p class="small">Submit a booking request. Your booking remains pending until confirmed by the lodge owner. No payment is taken online.</p>@if($errors->any())<div class="errors">{{ $errors->first() }}</div>@endif<form method="post" action="{{ route('lodge.book') }}">@csrf<input type="hidden" name="check_in" value="{{ $v['check_in'] }}"><input type="hidden" name="check_out" value="{{ $v['check_out'] }}"><input type="hidden" name="guests" value="{{ $v['guests'] }}"><div class="form-grid"><div><label>Name</label><input name="guest_name" required value="{{ old('guest_name') }}"></div><div><label>Email</label><input name="guest_email" type="email" required value="{{ old('guest_email') }}"></div><div class="full"><label>Phone (optional)</label><input name="guest_phone" value="{{ old('guest_phone') }}"></div><div class="full"><label>Anything we should know? (optional)</label><textarea name="notes" rows="4">{{ old('notes') }}</textarea></div><div class="full"><button type="submit">Request this stay →</button></div></div></form>@else<p>Someone has already requested or booked part of this stay. Please select alternative dates.</p><a class="button" href="{{ route('home') }}#book">Choose new dates</a>@endif</div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('lodge.layout')@section('title','Booking request received | Cresta 31')@section('content')<div class="subhero"><div class="wrap"><h1>Request received</h1></div></div><main class="section wrap"><div class="card"><div class="eyebrow">Cresta 31</div><h2>Thank you for your booking request.</h2><p>Your reference is <strong>{{ $booking->reference }}</strong>.</p><p>{{ $booking->check_in->format('j M Y') }} – {{ $booking->check_out->format('j M Y') }} · {{ $booking->nights }} nights</p><p class="price">£{{ number_format($booking->total_pence/100,2) }}</p><p><strong>Status:</strong> {{ ucfirst($booking->status) }}</p><p class="small">This is a request, not a confirmed reservation. No payment has been collected. Please retain your reference.</p><a class="button" href="{{ route('home') }}">Back to the lodge</a></div></main>@endsection
|
||||
@@ -0,0 +1 @@
|
||||
@extends('lodge.layout')@section('content')<section class="hero"><div class="wrap" style="width:100%"><div class="content"><div class="eyebrow">Unwind · Escape · Explore</div><h1>A little escape.<br>A lot to love.</h1><p>Welcome to Cresta 31 — a comfortable lodge retreat made for slow mornings, fresh air and memorable stays.</p><a class="button" style="background:#fff;color:#1b312b" href="#book">Explore your stay →</a></div></div></section><div class="wrap"><div class="booking" id="book" style="position:relative;margin-top:-45px"><form action="{{ route('lodge.availability') }}" method="get"><div class="booking-grid"><div><label for="check_in">Check in</label><input id="check_in" name="check_in" type="date" min="{{ date('Y-m-d') }}" required></div><div><label for="check_out">Check out</label><input id="check_out" name="check_out" type="date" min="{{ date('Y-m-d',strtotime('+1 day')) }}" required></div><div><label for="guests">Guests</label><select id="guests" name="guests">@for($i=1;$i<=config('lodge.max_guests');$i++)<option value="{{ $i }}">{{ $i }} {{ $i===1?'guest':'guests' }}</option>@endfor</select></div><button>Check dates →</button></div></form></div></div><section class="section" id="the-lodge"><div class="wrap intro"><div><div class="eyebrow" style="color:#b48d51">Welcome to Cresta 31</div><h2>Make yourself<br>right at home.</h2><p>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.</p><p>Browse the lodge photographs below, then choose your dates to check availability.</p><div class="price">From £{{ number_format($rate,0) }} <span class="small">/ night</span></div></div><img src="{{ asset('images/lodge/photo-02.png') }}" alt="Cresta 31 lodge interior" loading="lazy"></div></section><section class="section" id="gallery" style="background:#eaece6"><div class="wrap"><div class="eyebrow" style="color:#b48d51">A closer look</div><h2>Explore the lodge</h2><div class="gallery">@for($i=1;$i<=11;$i++)<a href="{{ asset(sprintf('images/lodge/photo-%02d.png',$i)) }}" target="_blank"><img src="{{ asset(sprintf('images/lodge/photo-%02d.png',$i)) }}" loading="lazy" alt="Cresta 31 lodge photo {{ $i }}"></a>@endfor</div></div></section><section class="section"><div class="wrap" style="text-align:center"><div class="eyebrow">Your next getaway</div><h2>Ready to stay?</h2><p class="small">Choose your dates and send a booking request.</p><a class="button" href="#book">Check availability →</a></div></section>@endsection
|
||||
@@ -0,0 +1,3 @@
|
||||
<!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/photo-01.png') 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></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>
|
||||
@@ -1,11 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\LodgeController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::view('/', 'welcome')->name('home');
|
||||
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::view('dashboard', 'dashboard')->name('dashboard');
|
||||
});
|
||||
|
||||
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');});
|
||||
require __DIR__.'/settings.php';
|
||||
|
||||