Sign in
diff --git a/src/components/HeaderNav.tsx b/src/components/HeaderNav.tsx
new file mode 100644
index 0000000..d4efe08
--- /dev/null
+++ b/src/components/HeaderNav.tsx
@@ -0,0 +1,304 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useCallback, useEffect, useState } from "react";
+import { useAuth } from "@/context/AuthContext";
+
+/** Desktop logged-out: same as before (no city line in header; full list in mobile drawer). */
+const guestNavDesktop = [
+ { href: "/#rooms", label: "Rooms" },
+ { href: "/guest", label: "Guest hub" },
+ { href: "/services", label: "Services" },
+ { href: "/#wellness", label: "Gym & Spa" },
+ { href: "/#dining", label: "Dining & venues" },
+ { href: "/#meetings", label: "Meetings" },
+ { href: "/#location", label: "Location" },
+];
+
+const guestNavMobile = [
+ { href: "/", label: "Home" },
+ ...guestNavDesktop,
+ { href: "/#tour", label: "3D tour" },
+];
+
+const servicesLinks = [
+ { href: "/guest", label: "Guest hub overview" },
+ { href: "/guest/room-service", label: "Room service" },
+ { href: "/guest/laundry", label: "Laundry" },
+ { href: "/guest/gym", label: "Gym" },
+ { href: "/guest/spa", label: "Spa" },
+ { href: "/services", label: "Spa & gym (services)" },
+];
+
+const amenitiesLinks = [
+ { href: "/#wellness", label: "Gym & wellness" },
+ { href: "/#dining", label: "Dining & venues" },
+ { href: "/#meetings", label: "Meetings & events" },
+ { href: "/#location", label: "Location" },
+ { href: "/#tour", label: "3D tour" },
+];
+
+function NavChevron() {
+ return (
+
+ );
+}
+
+function NavDropdown({
+ label,
+ items,
+ id,
+}: {
+ label: string;
+ items: { href: string; label: string }[];
+ id: string;
+}) {
+ return (
+
+
+ {label}
+
+
+
+
+ {items.map((item) => (
+ -
+
+ {item.label}
+
+
+ ))}
+
+
+
+ );
+}
+
+function HamburgerIcon() {
+ return (
+
+
+
+
+
+ );
+}
+
+function MobileNavDrawer({
+ open,
+ onClose,
+ session,
+}: {
+ open: boolean;
+ onClose: () => void;
+ session: ReturnType
["session"];
+}) {
+ const pathname = usePathname();
+
+ useEffect(() => {
+ onClose();
+ }, [pathname, onClose]);
+
+ useEffect(() => {
+ if (!open) return;
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ return () => {
+ document.body.style.overflow = prev;
+ };
+ }, [open]);
+
+ useEffect(() => {
+ if (!open) return;
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") onClose();
+ };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [open, onClose]);
+
+ if (!open) return null;
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export function HeaderNav() {
+ const { session, isHydrated } = useAuth();
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ const closeMobile = useCallback(() => setMobileOpen(false), []);
+
+ if (!isHydrated) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+ {/* Desktop */}
+ {session ? (
+
+ ) : (
+
+ )}
+
+ {/* Mobile hamburger */}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/RequireAuth.tsx b/src/components/RequireAuth.tsx
index 45a9917..6ff3d2d 100644
--- a/src/components/RequireAuth.tsx
+++ b/src/components/RequireAuth.tsx
@@ -8,7 +8,7 @@ import { ShitayeLogoLoader } from "@/components/ShitayeLogoLoader";
type Props = { children: React.ReactNode; redirectTo?: string };
-export function RequireAuth({ children, redirectTo = "/login" }: Props) {
+export function RequireAuth({ children, redirectTo = "/guest/login" }: Props) {
const { session, isHydrated } = useAuth();
const router = useRouter();
@@ -35,7 +35,10 @@ export function RequireAuth({ children, redirectTo = "/login" }: Props) {
return (
Redirecting to sign-in…
-
+
Continue manually
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
index 3111fe0..4a4dacf 100644
--- a/src/context/AuthContext.tsx
+++ b/src/context/AuthContext.tsx
@@ -1,314 +1,16 @@
-"use client";
-
-import {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
- type ReactNode,
-} from "react";
-import { DEMO_BOOKING_REFS } from "@/lib/mocks/guestData";
-
-const STORAGE_SESSION = "shitaye_session_v1";
-const STORAGE_ORDERS = "shitaye_orders_v1";
-
-export type OrderCategory = "room-service" | "laundry" | "gym" | "spa";
-
-export type OrderRecord = {
- id: string;
- category: OrderCategory;
- title: string;
- detail: string;
- totalUsd: number;
- placedAt: string;
- status: "pending" | "confirmed" | "completed";
-};
-
-export type MemberSession = {
- kind: "member";
- email: string;
- displayName: string;
- points: number;
- tier: "Gold" | "Silver";
- /** How they signed in — for display only */
- authMethod: "otp" | "password" | "google" | "apple" | "facebook";
-};
-
-export type BookingRefSession = {
- kind: "bookingRef";
- bookingRef: string;
- guestName: string;
- roomLabel: string;
- checkOut: string;
-};
-
-export type GuestSession = MemberSession | BookingRefSession;
-
-type AuthContextValue = {
- session: GuestSession | null;
- orders: OrderRecord[];
- isHydrated: boolean;
- /** Demo OTP is always 123456 */
- requestOtp: (email: string) => Promise<{ ok: boolean; message: string }>;
- verifyOtp: (email: string, code: string) => Promise<{ ok: boolean; message: string }>;
- loginPassword: (email: string, password: string) => Promise<{ ok: boolean; message: string }>;
- loginSocial: (provider: "google" | "apple" | "facebook") => void;
- loginBookingRef: (ref: string) => { ok: boolean; message: string };
- logout: () => void;
- addOrder: (o: Omit & { status?: OrderRecord["status"] }) => void;
- awardPoints: (points: number) => void;
-};
-
-const AuthContext = createContext(null);
-
-function loadOrders(): OrderRecord[] {
- if (typeof window === "undefined") return [];
- try {
- const raw = localStorage.getItem(STORAGE_ORDERS);
- if (!raw) return seedOrders();
- const parsed = JSON.parse(raw) as OrderRecord[];
- return Array.isArray(parsed) ? parsed : seedOrders();
- } catch {
- return seedOrders();
- }
-}
-
-function seedOrders(): OrderRecord[] {
- return [
- {
- id: "seed-rs-1",
- category: "room-service",
- title: "Room service · American breakfast ×2",
- detail: "Delivered 07:15 · Room charge",
- totalUsd: 36,
- placedAt: new Date(Date.now() - 86400000 * 2).toISOString(),
- status: "completed",
- },
- {
- id: "seed-l-1",
- category: "laundry",
- title: "Laundry · Express + 3 shirts",
- detail: "Returned same evening",
- totalUsd: 27,
- placedAt: new Date(Date.now() - 86400000).toISOString(),
- status: "completed",
- },
- {
- id: "seed-sp-1",
- category: "spa",
- title: "Spa · Signature Swedish 60 min",
- detail: "Apr 4 · 15:00",
- totalUsd: 85,
- placedAt: new Date(Date.now() - 86400000 * 3).toISOString(),
- status: "confirmed",
- },
- ];
-}
-
-function loadSession(): GuestSession | null {
- if (typeof window === "undefined") return null;
- try {
- const raw = localStorage.getItem(STORAGE_SESSION);
- if (!raw) return null;
- return JSON.parse(raw) as GuestSession;
- } catch {
- return null;
- }
-}
-
-function persistSession(s: GuestSession | null) {
- if (typeof window === "undefined") return;
- if (s) localStorage.setItem(STORAGE_SESSION, JSON.stringify(s));
- else localStorage.removeItem(STORAGE_SESSION);
-}
-
-function persistOrders(orders: OrderRecord[]) {
- if (typeof window === "undefined") return;
- localStorage.setItem(STORAGE_ORDERS, JSON.stringify(orders));
-}
-
-export function AuthProvider({ children }: { children: ReactNode }) {
- const [session, setSession] = useState(null);
- const [orders, setOrders] = useState([]);
- const [isHydrated, setIsHydrated] = useState(false);
-
- useEffect(() => {
- setSession(loadSession());
- setOrders(loadOrders());
- setIsHydrated(true);
- }, []);
-
- const requestOtp = useCallback(async (email: string) => {
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
- return { ok: false, message: "Enter a valid email address." };
- }
- return { ok: true, message: "Demo code sent. Use OTP 123456 to continue." };
- }, []);
-
- const verifyOtp = useCallback(async (email: string, code: string) => {
- const trimmed = code.replace(/\s/g, "");
- if (trimmed !== "123456") {
- return { ok: false, message: "Invalid code. Demo OTP is 123456." };
- }
- const local = email.split("@")[0] ?? "Guest";
- const name = local.charAt(0).toUpperCase() + local.slice(1);
- const next: MemberSession = {
- kind: "member",
- email: email.toLowerCase(),
- displayName: name,
- points: 2400,
- tier: "Gold",
- authMethod: "otp",
- };
- setSession(next);
- persistSession(next);
- return { ok: true, message: "Signed in." };
- }, []);
-
- const loginPassword = useCallback(async (email: string, password: string) => {
- if (!email || !password) {
- return { ok: false, message: "Email and password required." };
- }
- if (password !== "shitaye" && password !== "demo123") {
- return {
- ok: false,
- message: "Incorrect password. Try demo password: shitaye",
- };
- }
- const local = email.split("@")[0] ?? "Guest";
- const name = local.charAt(0).toUpperCase() + local.slice(1);
- const next: MemberSession = {
- kind: "member",
- email: email.toLowerCase(),
- displayName: name,
- points: 2400,
- tier: "Gold",
- authMethod: "password",
- };
- setSession(next);
- persistSession(next);
- return { ok: true, message: "Signed in." };
- }, []);
-
- const loginSocial = useCallback((provider: "google" | "apple" | "facebook") => {
- const names: Record = {
- google: "Google Guest",
- apple: "Apple Guest",
- facebook: "Facebook Guest",
- };
- const next: MemberSession = {
- kind: "member",
- email: `guest.${provider}@shitaye.demo`,
- displayName: names[provider],
- points: 2100,
- tier: "Silver",
- authMethod: provider,
- };
- setSession(next);
- persistSession(next);
- }, []);
-
- const loginBookingRef = useCallback((ref: string) => {
- const key = ref.trim().toUpperCase();
- const row = DEMO_BOOKING_REFS[key];
- if (!row) {
- return {
- ok: false,
- message: "Reference not found. Try SHITAYE-2026-DEMO or GUEST-1234.",
- };
- }
- const next: BookingRefSession = {
- kind: "bookingRef",
- bookingRef: key,
- guestName: row.guestName,
- roomLabel: row.room,
- checkOut: row.checkOut,
- };
- setSession(next);
- persistSession(next);
- return { ok: true, message: "Linked to your stay." };
- }, []);
-
- const logout = useCallback(() => {
- setSession(null);
- persistSession(null);
- }, []);
-
- const addOrder = useCallback(
- (
- o: Omit & {
- status?: OrderRecord["status"];
- },
- ) => {
- const rec: OrderRecord = {
- ...o,
- id: `ord-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
- placedAt: new Date().toISOString(),
- status: o.status ?? "pending",
- };
- setOrders((prev) => {
- const next = [rec, ...prev];
- persistOrders(next);
- return next;
- });
- if (session?.kind === "member") {
- const bonus = Math.min(150, Math.round(o.totalUsd * 2));
- setSession((s) => {
- if (!s || s.kind !== "member") return s;
- const u = { ...s, points: s.points + bonus };
- persistSession(u);
- return u;
- });
- }
- },
- [session],
- );
-
- const awardPoints = useCallback((points: number) => {
- setSession((s) => {
- if (!s || s.kind !== "member") return s;
- const u = { ...s, points: s.points + points };
- persistSession(u);
- return u;
- });
- }, []);
-
- const value = useMemo(
- () => ({
- session,
- orders,
- isHydrated,
- requestOtp,
- verifyOtp,
- loginPassword,
- loginSocial,
- loginBookingRef,
- logout,
- addOrder,
- awardPoints,
- }),
- [
- session,
- orders,
- isHydrated,
- requestOtp,
- verifyOtp,
- loginPassword,
- loginSocial,
- loginBookingRef,
- logout,
- addOrder,
- awardPoints,
- ],
- );
-
- return {children};
-}
-
-export function useAuth() {
- const ctx = useContext(AuthContext);
- if (!ctx) throw new Error("useAuth must be used within AuthProvider");
- return ctx;
-}
+/**
+ * Back-compat re-exports — canonical implementation is GuestSessionContext.
+ */
+export {
+ GuestSessionProvider,
+ GuestSessionProvider as AuthProvider,
+ useAuth,
+ useGuestSession,
+} from "./GuestSessionContext";
+export type {
+ BookingRefSession,
+ GuestSession,
+ MemberSession,
+ OrderCategory,
+ OrderRecord,
+} from "./GuestSessionContext";
diff --git a/src/context/GuestSessionContext.tsx b/src/context/GuestSessionContext.tsx
new file mode 100644
index 0000000..39e0145
--- /dev/null
+++ b/src/context/GuestSessionContext.tsx
@@ -0,0 +1,269 @@
+"use client";
+
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import {
+ loadOrders as repoLoadOrders,
+ loadSession as repoLoadSession,
+ saveOrders as repoSaveOrders,
+ saveSession as repoSaveSession,
+} from "@/lib/guest/repository";
+import type {
+ BookingRefSession,
+ GuestSession,
+ MemberSession,
+ OrderRecord,
+} from "@/lib/guest/types";
+import { DEMO_BOOKING_REFS } from "@/lib/mocks/guestData";
+import { lookupGuestBooking } from "@/lib/mocks/guestBookings";
+
+export type {
+ BookingRefSession,
+ GuestSession,
+ MemberSession,
+ OrderCategory,
+ OrderRecord,
+} from "@/lib/guest/types";
+
+type GuestSessionContextValue = {
+ session: GuestSession | null;
+ orders: OrderRecord[];
+ isHydrated: boolean;
+ requestOtp: (email: string) => Promise<{ ok: boolean; message: string }>;
+ verifyOtp: (email: string, code: string) => Promise<{ ok: boolean; message: string }>;
+ loginPassword: (email: string, password: string) => Promise<{ ok: boolean; message: string }>;
+ loginSocial: (provider: "google" | "apple" | "facebook") => void;
+ loginBookingRef: (ref: string, lastName?: string) => { ok: boolean; message: string };
+ logout: () => void;
+ addOrder: (
+ o: Omit & { status?: OrderRecord["status"] },
+ ) => void;
+ awardPoints: (points: number) => void;
+};
+
+const GuestSessionContext = createContext(null);
+
+export function GuestSessionProvider({ children }: { children: ReactNode }) {
+ const [session, setSession] = useState(null);
+ const [orders, setOrders] = useState([]);
+ const [isHydrated, setIsHydrated] = useState(false);
+
+ useEffect(() => {
+ setSession(repoLoadSession());
+ setOrders(repoLoadOrders());
+ setIsHydrated(true);
+ }, []);
+
+ const requestOtp = useCallback(async (email: string) => {
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ return { ok: false, message: "Enter a valid email address." };
+ }
+ return { ok: true, message: "Demo code sent. Use OTP 123456 to continue." };
+ }, []);
+
+ const verifyOtp = useCallback(async (email: string, code: string) => {
+ const trimmed = code.replace(/\s/g, "");
+ if (trimmed !== "123456") {
+ return { ok: false, message: "Invalid code. Demo OTP is 123456." };
+ }
+ const local = email.split("@")[0] ?? "Guest";
+ const name = local.charAt(0).toUpperCase() + local.slice(1);
+ const next: MemberSession = {
+ kind: "member",
+ email: email.toLowerCase(),
+ displayName: name,
+ points: 2400,
+ tier: "Gold",
+ authMethod: "otp",
+ };
+ setSession(next);
+ repoSaveSession(next);
+ return { ok: true, message: "Signed in." };
+ }, []);
+
+ const loginPassword = useCallback(async (email: string, password: string) => {
+ if (!email || !password) {
+ return { ok: false, message: "Email and password required." };
+ }
+ if (password !== "shitaye" && password !== "demo123") {
+ return {
+ ok: false,
+ message: "Incorrect password. Try demo password: shitaye",
+ };
+ }
+ const local = email.split("@")[0] ?? "Guest";
+ const name = local.charAt(0).toUpperCase() + local.slice(1);
+ const next: MemberSession = {
+ kind: "member",
+ email: email.toLowerCase(),
+ displayName: name,
+ points: 2400,
+ tier: "Gold",
+ authMethod: "password",
+ };
+ setSession(next);
+ repoSaveSession(next);
+ return { ok: true, message: "Signed in." };
+ }, []);
+
+ const loginSocial = useCallback((provider: "google" | "apple" | "facebook") => {
+ const names: Record = {
+ google: "Google Guest",
+ apple: "Apple Guest",
+ facebook: "Facebook Guest",
+ };
+ const next: MemberSession = {
+ kind: "member",
+ email: `guest.${provider}@shitaye.demo`,
+ displayName: names[provider],
+ points: 2100,
+ tier: "Silver",
+ authMethod: provider,
+ };
+ setSession(next);
+ repoSaveSession(next);
+ }, []);
+
+ const loginBookingRef = useCallback((refRaw: string, lastName?: string) => {
+ const key = refRaw.trim().toUpperCase();
+
+ if (key.startsWith("PAY-") || key.startsWith("SHY-")) {
+ if (!lastName?.trim()) {
+ return {
+ ok: false,
+ message: "Last name is required for confirmation / hold references.",
+ };
+ }
+ const row = lookupGuestBooking(refRaw, lastName);
+ if (!row) {
+ return {
+ ok: false,
+ message:
+ "Reference and last name do not match. Try PAY-MOCK-CONFIRMED + last name Demo, or SHY-MOCK-HOLD + Hold.",
+ };
+ }
+ const next: BookingRefSession = {
+ kind: "bookingRef",
+ bookingRef: row.ref,
+ guestName: row.guestName,
+ roomLabel: row.roomLabel,
+ checkOut: row.checkOut,
+ };
+ setSession(next);
+ repoSaveSession(next);
+ return { ok: true, message: "Linked to your stay." };
+ }
+
+ const legacy = DEMO_BOOKING_REFS[key];
+ if (!legacy) {
+ return {
+ ok: false,
+ message:
+ "Reference not found. Use SHITAYE-2026-DEMO, GUEST-1234, or PAY-MOCK-CONFIRMED with last name Demo.",
+ };
+ }
+ const next: BookingRefSession = {
+ kind: "bookingRef",
+ bookingRef: key,
+ guestName: legacy.guestName,
+ roomLabel: legacy.room,
+ checkOut: legacy.checkOut,
+ };
+ setSession(next);
+ repoSaveSession(next);
+ return { ok: true, message: "Linked to your stay." };
+ }, []);
+
+ const logout = useCallback(() => {
+ setSession(null);
+ repoSaveSession(null);
+ }, []);
+
+ const addOrder = useCallback(
+ (
+ o: Omit & {
+ status?: OrderRecord["status"];
+ },
+ ) => {
+ const rec: OrderRecord = {
+ ...o,
+ id: `ord-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
+ placedAt: new Date().toISOString(),
+ status: o.status ?? "pending",
+ };
+ setOrders((prev) => {
+ const next = [rec, ...prev];
+ repoSaveOrders(next);
+ return next;
+ });
+ setSession((s) => {
+ if (!s || s.kind !== "member") return s;
+ const bonus = Math.min(150, Math.round(o.totalUsd * 2));
+ const u = { ...s, points: s.points + bonus };
+ repoSaveSession(u);
+ return u;
+ });
+ },
+ [],
+ );
+
+ const awardPoints = useCallback((points: number) => {
+ setSession((s) => {
+ if (!s || s.kind !== "member") return s;
+ const u = { ...s, points: s.points + points };
+ repoSaveSession(u);
+ return u;
+ });
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ session,
+ orders,
+ isHydrated,
+ requestOtp,
+ verifyOtp,
+ loginPassword,
+ loginSocial,
+ loginBookingRef,
+ logout,
+ addOrder,
+ awardPoints,
+ }),
+ [
+ session,
+ orders,
+ isHydrated,
+ requestOtp,
+ verifyOtp,
+ loginPassword,
+ loginSocial,
+ loginBookingRef,
+ logout,
+ addOrder,
+ awardPoints,
+ ],
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useGuestSession() {
+ const ctx = useContext(GuestSessionContext);
+ if (!ctx) throw new Error("useGuestSession must be used within GuestSessionProvider");
+ return ctx;
+}
+
+/** @deprecated Prefer useGuestSession — alias kept for existing imports */
+export function useAuth() {
+ return useGuestSession();
+}
diff --git a/src/lib/guest/repository.ts b/src/lib/guest/repository.ts
new file mode 100644
index 0000000..9c4c419
--- /dev/null
+++ b/src/lib/guest/repository.ts
@@ -0,0 +1,77 @@
+/**
+ * localStorage persistence for guest session and orders.
+ * Replace with D1 / REST / Supabase behind the same interface in Phase 2.
+ */
+import type { GuestSession, OrderRecord } from "./types";
+
+export const STORAGE_SESSION_KEY = "shitaye_session_v1";
+export const STORAGE_ORDERS_KEY = "shitaye_orders_v1";
+
+export function loadSession(): GuestSession | null {
+ if (typeof window === "undefined") return null;
+ try {
+ const raw = localStorage.getItem(STORAGE_SESSION_KEY);
+ if (!raw) return null;
+ return JSON.parse(raw) as GuestSession;
+ } catch {
+ return null;
+ }
+}
+
+export function saveSession(session: GuestSession | null): void {
+ if (typeof window === "undefined") return;
+ if (session) {
+ localStorage.setItem(STORAGE_SESSION_KEY, JSON.stringify(session));
+ } else {
+ localStorage.removeItem(STORAGE_SESSION_KEY);
+ }
+}
+
+export function loadOrders(): OrderRecord[] {
+ if (typeof window === "undefined") return [];
+ try {
+ const raw = localStorage.getItem(STORAGE_ORDERS_KEY);
+ if (!raw) return seedOrders();
+ const parsed = JSON.parse(raw) as OrderRecord[];
+ return Array.isArray(parsed) ? parsed : seedOrders();
+ } catch {
+ return seedOrders();
+ }
+}
+
+export function saveOrders(orders: OrderRecord[]): void {
+ if (typeof window === "undefined") return;
+ localStorage.setItem(STORAGE_ORDERS_KEY, JSON.stringify(orders));
+}
+
+function seedOrders(): OrderRecord[] {
+ return [
+ {
+ id: "seed-rs-1",
+ category: "room-service",
+ title: "Room service · American breakfast ×2",
+ detail: "Delivered 07:15 · Room charge",
+ totalUsd: 36,
+ placedAt: new Date(Date.now() - 86400000 * 2).toISOString(),
+ status: "completed",
+ },
+ {
+ id: "seed-l-1",
+ category: "laundry",
+ title: "Laundry · Express + 3 shirts",
+ detail: "Returned same evening",
+ totalUsd: 27,
+ placedAt: new Date(Date.now() - 86400000).toISOString(),
+ status: "completed",
+ },
+ {
+ id: "seed-sp-1",
+ category: "spa",
+ title: "Spa · Signature Swedish 60 min",
+ detail: "Apr 4 · 15:00",
+ totalUsd: 85,
+ placedAt: new Date(Date.now() - 86400000 * 3).toISOString(),
+ status: "confirmed",
+ },
+ ];
+}
diff --git a/src/lib/guest/types.ts b/src/lib/guest/types.ts
new file mode 100644
index 0000000..8d8adf7
--- /dev/null
+++ b/src/lib/guest/types.ts
@@ -0,0 +1,34 @@
+/** Guest portal order and session types (Phase 1: client-only; swap repository for API later). */
+
+export type OrderCategory = "room-service" | "laundry" | "gym" | "spa";
+
+export type OrderRecord = {
+ id: string;
+ category: OrderCategory;
+ title: string;
+ detail: string;
+ totalUsd: number;
+ placedAt: string;
+ status: "pending" | "confirmed" | "completed";
+};
+
+/** Full account — email / OTP / password / social */
+export type MemberSession = {
+ kind: "member";
+ email: string;
+ displayName: string;
+ points: number;
+ tier: "Gold" | "Silver";
+ authMethod: "otp" | "password" | "google" | "apple" | "facebook";
+};
+
+/** Limited scope — booking reference only */
+export type BookingRefSession = {
+ kind: "bookingRef";
+ bookingRef: string;
+ guestName: string;
+ roomLabel: string;
+ checkOut: string;
+};
+
+export type GuestSession = MemberSession | BookingRefSession;
diff --git a/src/lib/mocks/guestBookings.ts b/src/lib/mocks/guestBookings.ts
new file mode 100644
index 0000000..f5173b5
--- /dev/null
+++ b/src/lib/mocks/guestBookings.ts
@@ -0,0 +1,49 @@
+/**
+ * Mock allowlist for confirmation / hold references (PAY-*, SHY-*) + last name check.
+ * Aligns with mock IDs from `src/lib/mocks/api.ts` style.
+ */
+export type GuestBookingRecord = {
+ ref: string;
+ /** Normalized uppercase last name for demo validation */
+ lastNameKey: string;
+ guestName: string;
+ roomLabel: string;
+ checkOut: string;
+};
+
+export const guestBookingAllowlist: GuestBookingRecord[] = [
+ {
+ ref: "PAY-MOCK-CONFIRMED",
+ lastNameKey: "DEMO",
+ guestName: "Demo Guest",
+ roomLabel: "Junior Studio · 1204",
+ checkOut: "2026-04-12",
+ },
+ {
+ ref: "SHY-MOCK-HOLD",
+ lastNameKey: "HOLD",
+ guestName: "Hold Guest",
+ roomLabel: "Standard King · 602",
+ checkOut: "2026-04-10",
+ },
+];
+
+function normalize(s: string): string {
+ return s.trim().toUpperCase().replace(/[^A-Z]/g, "");
+}
+
+/**
+ * Match booking reference + last name against allowlist.
+ */
+export function lookupGuestBooking(
+ refRaw: string,
+ lastNameRaw: string,
+): GuestBookingRecord | null {
+ const ref = refRaw.trim().toUpperCase();
+ const last = normalize(lastNameRaw);
+ if (!ref || !last) return null;
+ return (
+ guestBookingAllowlist.find((r) => r.ref.toUpperCase() === ref && r.lastNameKey === last) ??
+ null
+ );
+}