youbahyt@gmail.com Casablanca --:--

Home/Work/02

Locandwork

Book a desk in Casablanca by the hour, the day or the month — and never get the same desk twice.

RoleSolo — design, API and front-end
ContextInternship project
WhenMay — July 2026, 19 commits
StackLaravel 11 · Vue 3 · Pinia · Sanctum · Bootstrap 5
Locandwork home page: 'Find a room for your ambition.' on forest green, with a search box for workspace type, duration and city

(01)

Three sides, one booking

Locandwork is an Airbnb-style marketplace for workspaces. Customers search and book. Hosts list a space and split it into workspaces — hot desks, dedicated desks, private offices, meeting rooms — each with its own capacity and hourly, daily and monthly price in dirhams. Admins approve or reject listings before anyone can book them.

I did all of it: the schema, the API, the Vue app, and the "Linen & Forest" look with its own logo.

API48

routes under /api/v1

Models12

Eloquent models

Views30

Vue components

Schema19

migrations

Locandwork admin space approvals: a table of Casablanca spaces with host, city, live or pending status, and approve or reject buttons
Admin — space approvals/admin/spaces

(02)

No double bookings — enforced by the database

Checking availability and then inserting is a race: two people can pass the check at the same moment. So the check isn't in PHP. Every booked night becomes a row in booking_days, and that table has a unique index on workspace + day.

// database/migrations/…_create_booking_days_table.php
// same workspace cannot have two bookings on the same day
$table->unique(['space_workspace_id', 'day']);

The booking service locks the workspace row, creates the booking, then inserts one row per night. If any night is taken, the insert fails, the whole transaction rolls back, and the customer gets a clean 422 instead of a half-made booking.

// app/Services/BookingService.php (trimmed)
return DB::transaction(function () use (…) {
    SpaceWorkspace::where('id', $workspace->id)->lockForUpdate()->first();

    $booking = Booking::create([…]);

    try {
        DB::table('booking_days')->insert($rows);   // one row per night
    } catch (QueryException $e) {
        if ($this->isDuplicateKey($e)) {
            throw ValidationException::withMessages([
                'start_date' => ['One or more of these dates are no longer available.'],
            ]);
        }
        throw $e;
    }

    return $booking;
});

Two small decisions make it hold up. Check-out day isn't inserted — the hotel model — so back-to-back stays don't collide. And the duplicate-key check recognises both MySQL's error 1062 and SQLite's message, so the same guard works in production and in tests.

(03)

Tools for hosts

Hosts create a listing with all of its workspaces in one form — one database transaction, so a listing never exists without its desks — then upload photos and wait for approval. Incoming bookings arrive as pending; the host confirms or cancels from a single table.

Locandwork host bookings table with status filters and confirm / cancel buttons on a pending booking
Locandwork booking widget: 900 MAD per day, hourly/daily/monthly toggle, workspace types to choose from
Host bookings · booking widget on a space pageSeeded demo data

(04)

Small things that add up

  • Slugs everywhereSpaces resolve by slug on every route group — public, host, admin and wishlist.
  • Idempotent wishlistSaving twice never throws; a unique pair plus syncWithoutDetaching.
  • Token authSanctum bearer tokens, one Axios instance, a 401 interceptor that signs you out.
  • Route guardsVue Router rehydrates the user and sends each role to its own dashboard.
  • Loading statesSkeleton shimmers and empty states on every list, in the brand's own tones.
  • Realistic seedNine Casablanca spaces, two hosts, eleven bookings — the screenshots here use it.

(05)

What's next

Stripe Connect so hosts get paid out automatically, a messages UI on top of the threads table that already exists, reviews after a stay, and a proper edit flow for listings. Hourly bookings currently block the whole day — true per-hour slots are the next change to the booking service.

Next case study — 01 Desk+ →

Want this on your team?