`frontend-villabooking`: The Booking App That Treats Identity as a Workflow
A React frontend with a role-gated onboarding path, SSR-safe persistence, proxy-backed API calls, and booking state built for time slots, not shopping carts.
- This repo makes identity a transition state, using `/welcome` to separate authenticated users who have not yet chosen a role from travelers and hosts.
- Its access control is centralized, so routing, flicker prevention, and host-only gating behave like one control plane instead of scattered page checks.
- The frontend is built for SSR and deployment realities, with persistence guards and rewrite-based API calls that avoid browser-only assumptions.
- The booking model stores intent over inventory, which makes dates, villa IDs, and availability feel like a reservation ledger instead of a shopping cart.
The app does not just log you in. It decides who you are becoming.
Most booking frontends start with listings. This one starts with a role decision. A signed-in user who has not chosen between Traveler and Host is not dropped into a half-baked dashboard. They are routed to /welcome, where identity becomes a product step instead of an assumption.
That is a small UX choice with a big architectural effect. It gives the app a clean story for permissions, navigation, and state. The user is not merely authenticated. They are in motion toward a context the app can trust.
This is a React application built with TypeScript, Vite, and Tailwind CSS.
RouteProtection is the real product logic
| Traditional role signup | This repo's staged identity flow |
|---|---|
| User picks a role up front. | User enters a neutral authenticated state first. |
| Permissions are implied by signup choice. | Permissions are confirmed after onboarding at /welcome. |
| Protected content can flicker before redirect. | A checking state suppresses flicker before the route resolves. |
| Role mistakes happen early and silently. | Role choice becomes visible, intentional, and reversible in the flow. |
The important detail is not only the redirect. It is the fact that the app has a dedicated checking state. That means protected content does not flash on screen before the guard settles. For a routing system, that is the difference between looking secure and actually feeling secure.
Centralizing this logic also keeps host-only routes honest. A traveler does not get to wander into admin territory just because they know the URL. The app enforces the contract once, then reuses it everywhere.
State persistence is handled like an SSR problem, not a browser trick
The repo treats persistence as an environment problem, not a convenience problem. Redux state is persisted, but the implementation still has to behave when localStorage is unavailable during server rendering. That is where the Noop Storage pattern matters: it prevents hydration from becoming a bug factory.
// storage.ts pattern used to avoid SSR hydration errors
const createNoopStorage = () => ({
getItem() {
return Promise.resolve(null)
},
setItem(_key: string, value: string) {
return Promise.resolve(value)
},
removeItem(_key: string) {
return Promise.resolve()
},
})
const storage = typeof window !== 'undefined'
? localStorage
: createNoopStorage()
That choice is especially useful here because both auth and cart state need to survive refreshes. The frontend can remember who you are and what you were booking without assuming the browser exists at render time. In other words, the app is careful about where state lives before it asks what state means.
The cart is not a cart. It is a temporary booking ledger
| Physical shopping cart | Villa booking ledger |
|---|---|
| Stores items you own. | Stores dates and intent you may later confirm. |
| Quantity is the main variable. | Check-in, check-out, and availability are the main variables. |
| Inventory is stable after add-to-cart. | The value changes as time windows and property data change. |
| Checkout completes a purchase. | Checkout commits a reservation state. |
That domain model matters because villas are not products in the retail sense. The app works with time windows, guest counts, and availability. It is less about accumulating goods and more about negotiating a stay.
The filter layer follows the same logic. Query state is built from URLSearchParams, which makes search and sharing more natural. The app is writing booking intention into the URL, not hiding it in component state.
The backend disappears behind the frontend
The frontend avoids hardcoding backend addresses by leaning on Next.js rewrites and a shared Axios instance. Calls can hit /api/villas while the framework forwards them to the real server behind the scenes. That keeps local development, deployment, and CORS handling from leaking into product code.
// Axios instance can stay stable while rewrites route traffic elsewhere
import axios from 'axios'
export const api = axios.create({
baseURL: '/api',
withCredentials: true,
})
export const fetchVillas = () => api.get('/villas')
This is the kind of plumbing that disappears when it works. Which is exactly the point. The app can change environments without rewriting every request path, and the developer experience stays calm even as the backend moves around.
Auth is hybrid, but the experience stays coherent
The auth flow mixes credentials and Google OAuth, but the user experience does not fragment. The redirect is built from window.location.origin, which keeps the flow aligned with whatever host the app is running on. That is a practical detail, but it is also a sign of discipline.
\"dependencies\": {\n \"@reduxjs/toolkit\": \"^2.2.1\",\n \"lucide-react\": \"^0.344.0\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"react-redux\": \"^9.1.0\",\n \"react-router-dom\": \"^6.22.2\"\n },
The stack is not trying to impress with novelty. It is trying to hold together. Redux Toolkit, React Router, TypeScript, and a modern build system are doing unglamorous work in service of a coherent app.
Why this repo stands out among booking frontends
| Older booking frontend template | frontend-villabooking |
|---|---|
| Often CRA-based and loosely typed. | Built with a more modern React toolchain and TypeScript. |
| Role handling is usually flat or absent. | Role routing is staged and explicit. |
| Persistence is often browser-first. | Persistence is SSR-aware. |
| API wiring is often hardcoded. | API calls are insulated behind rewrites and Axios defaults. |
There are plenty of booking demos that can browse a villa and submit a form. Fewer of them make identity, persistence, and routing feel like one system. That is what makes this repo memorable. It is not bigger than the alternatives. It is more deliberate.