diff --git a/src/app/login/LoginPageClient.tsx b/src/app/login/LoginPageClient.tsx index 8244f35..784f7aa 100644 --- a/src/app/login/LoginPageClient.tsx +++ b/src/app/login/LoginPageClient.tsx @@ -12,13 +12,7 @@ export function LoginPageClient() { const searchParams = useSearchParams(); const nextPath = searchParams.get("next") || "/profile"; - const { - requestOtp, - verifyOtp, - loginPassword, - loginSocial, - loginBookingRef, - } = useAuth(); + const { requestOtp, verifyOtp, loginPassword, loginGoogle, loginBookingRef } = useAuth(); const [tab, setTab] = useState("otp"); const [email, setEmail] = useState(""); @@ -59,15 +53,17 @@ export function LoginPageClient() { if (r.ok) router.push(nextPath); } - function handleSocial(provider: "google" | "apple" | "facebook") { - loginSocial(provider); - router.push(nextPath); + async function handleGoogle() { + setMessage(null); + await loginGoogle(); } - function handleBookingRef(e: React.FormEvent) { + async function handleBookingRef(e: React.FormEvent) { e.preventDefault(); setMessage(null); - const r = loginBookingRef(bookingRef); + setLoading(true); + const r = await loginBookingRef(bookingRef); + setLoading(false); setMessage(r.message); if (r.ok) router.push(nextPath); } @@ -162,7 +158,7 @@ export function LoginPageClient() { />

- Demo: enter 123456 + Use the code sent to your email (hotel guest OTP).

- -
)} @@ -269,11 +253,15 @@ export function LoginPageClient() { />

- Try SHITAYE-2026-DEMO or GUEST-1234 — no email - required. You can place orders and view a limited stay profile. + Enter the booking code from your confirmation email. You must have used the same + email at booking so your account links for the full guest portal.

- )} diff --git a/src/app/profile/ProfilePageClient.tsx b/src/app/profile/ProfilePageClient.tsx index f45ced1..c1a9fc7 100644 --- a/src/app/profile/ProfilePageClient.tsx +++ b/src/app/profile/ProfilePageClient.tsx @@ -1,16 +1,18 @@ "use client"; import Link from "next/link"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RequireAuth } from "@/components/RequireAuth"; import { useAuth } from "@/context/AuthContext"; import type { OrderCategory, OrderRecord } from "@/context/AuthContext"; import { - seedAppointments, - seedRewardsHistory, - seedShuttle, -} from "@/lib/mocks/guestData"; -import { siteConfig } from "@/lib/mocks/site"; + guestMe, + guestOrders, + guestPointsHistory, + guestSpaBookings, + type PointLedgerRow, + type SpaBookingRow, +} from "@/lib/guest-hotel-api"; const orderTabs: { id: OrderCategory | "all"; label: string }[] = [ { id: "all", label: "All" }, @@ -60,13 +62,66 @@ export function ProfilePageClient() { } function ProfileContent() { - const { session, orders, logout } = useAuth(); + const { session, logout, accessToken } = useAuth(); const [orderFilter, setOrderFilter] = useState("all"); + const [apiBalance, setApiBalance] = useState(null); + const [apiLedger, setApiLedger] = useState([]); + const [apiOrders, setApiOrders] = useState([]); + const [appointments, setAppointments] = useState([]); + + useEffect(() => { + if (!accessToken || !session) return; + const pid = session.propertyId; + if (!pid) return; + let cancelled = false; + (async () => { + try { + const me = await guestMe(pid, accessToken); + const ph = await guestPointsHistory(pid, accessToken); + const ord = await guestOrders(pid, accessToken); + const spa = await guestSpaBookings(pid, accessToken); + + if (!cancelled) { + setApiBalance(me.balance); + setApiLedger(ph.data ?? []); + setApiOrders( + (ord.data ?? []).map((o) => ({ + id: o.id, + category: o.type, + title: + o.type === "room-service" + ? "Room Service Order" + : o.type === "laundry" + ? "Laundry Request" + : o.type === "gym" + ? "Gym Booking" + : "Spa Booking", + detail: o.detail, + totalUsd: Number(o.total ?? 0), + placedAt: o.createdAt, + status: (["pending", "confirmed", "completed"].includes(o.status.toLowerCase()) + ? o.status.toLowerCase() + : "pending") as OrderRecord["status"], + })), + ); + setAppointments(spa.data ?? []); + } + } catch { + if (!cancelled) { + setApiOrders([]); + setAppointments([]); + } + } + })(); + return () => { + cancelled = true; + }; + }, [accessToken, session]); const filteredOrders = useMemo(() => { - if (orderFilter === "all") return orders; - return orders.filter((o) => o.category === orderFilter); - }, [orders, orderFilter]); + if (orderFilter === "all") return apiOrders; + return apiOrders.filter((o) => o.category === orderFilter); + }, [apiOrders, orderFilter]); if (!session) { return null; @@ -86,24 +141,19 @@ function ProfileContent() {

- {session.kind === "member" - ? `Hello, ${session.displayName}` - : `Welcome, ${session.guestName}`} + Hello, {session.displayName}

- {session.kind === "member" ? ( + {session.email} + {session.bookingCode ? ( <> - {session.email} {" · "} - Signed in via {session.authMethod} + Booking code{" "} + + {session.bookingCode} + - ) : ( - <> - Booking {session.bookingRef} - {" · "} - {session.roomLabel} · checkout {session.checkOut} - - )} + ) : null}

@@ -125,75 +175,49 @@ function ProfileContent() {

Rewards points

- {session.kind === "member" ? ( - <> -

- {session.points.toLocaleString()} -

-

- {session.tier} tier · earn on stays & dining -

- - ) : ( -

- Full loyalty points unlock when you sign in with email. Booking-ID access covers - orders and stay tools. + <> +

+ {(apiBalance ?? session.points).toLocaleString()}

- )} -
- -
-

- Airport shuttle -

-

- Lobby pickup · {seedShuttle.lobbyPickupTime} -

-

- {new Date(seedShuttle.departureDate).toLocaleDateString(undefined, { - weekday: "long", - month: "long", - day: "numeric", - })}{" "} - · {seedShuttle.airport} -

-

- {seedShuttle.flightLabel} -

-

{seedShuttle.notes}

- - Request a change - +

+ {apiBalance != null ? "Live balance" : "Balance unavailable"} +

+

Booked appointments

-
    - {seedAppointments.map((a) => ( -
  • -

    - {a.status} -

    -

    {a.title}

    -

    {a.when}

    -

    {a.where}

    -
  • - ))} -
+ {appointments.length === 0 ? ( +

+ No gym/spa bookings found. +

+ ) : ( +
    + {appointments.map((a) => ( +
  • +

    + {a.status} +

    +

    + {a.offering?.name ?? "Spa/Gym booking"} +

    +

    + {formatWhen(a.scheduledAt ?? a.createdAt)} +

    +
  • + ))} +
+ )}

Orders

- Room service, laundry, gym, and spa — including demo history and new orders from this - device. + Room service, laundry, gym, and spa

{orderTabs.map((t) => ( @@ -225,21 +249,33 @@ function ProfileContent() {
-

Rewards earned

+

Rewards history

    - {seedRewardsHistory.map((r) => ( -
  • -
    -

    {r.label}

    -

    {r.earnedAt}

    -
    - +{r.points} pts -
  • - ))} + {apiLedger.length > 0 + ? apiLedger.map((r) => ( +
  • +
    +

    {r.reason.replace(/_/g, " ")}

    +

    {formatWhen(r.createdAt)}

    +
    + = 0 ? "badge-mustard" : "rounded-full bg-red-100 px-3 py-1 text-xs font-semibold text-red-800" + } + > + {r.delta >= 0 ? "+" : ""} + {r.delta} pts + +
  • + )) + : null}
+ {apiLedger.length === 0 ? ( +

No rewards history returned yet.

+ ) : null}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index d5955b1..493e858 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; import Link from "next/link"; -import { siteConfig } from "@/lib/mocks/site"; +import { siteConfig } from "@/lib/site-config"; export function Footer() { return ( diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a942ee0..60b79e0 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,9 +1,12 @@ +"use client"; + import Image from "next/image"; import Link from "next/link"; import { HeaderAccount } from "@/components/HeaderAccount"; import { CurrencySwitcher } from "@/components/CurrencySwitcher"; import { ReviewsMenu } from "@/components/ReviewsMenu"; -import { siteConfig } from "@/lib/mocks/site"; +import { siteConfig } from "@/lib/site-config"; +import { useAuth } from "@/context/AuthContext"; const nav = [ { href: "/#rooms", label: "Rooms" }, @@ -16,6 +19,8 @@ const nav = [ ]; export function Header() { + const { session } = useAuth(); + return (
@@ -72,12 +77,14 @@ export function Header() {
- - Book - + {!session && ( + + Book + + )}
diff --git a/src/components/HeaderAccount.tsx b/src/components/HeaderAccount.tsx index bc42d7c..19f0253 100644 --- a/src/components/HeaderAccount.tsx +++ b/src/components/HeaderAccount.tsx @@ -13,12 +13,8 @@ export function HeaderAccount() { } if (session) { - const points = - session.kind === "member" ? session.points : "—"; - const label = - session.kind === "member" - ? session.displayName.split(" ")[0] ?? "Guest" - : session.guestName.split(" ")[0] ?? "Guest"; + const points = session.points; + const label = session.displayName.split(" ")[0] ?? "Guest"; return (
@@ -27,7 +23,7 @@ export function HeaderAccount() { className="hidden max-w-[140px] truncate rounded-full border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3 py-1.5 text-xs font-semibold text-[var(--color-primary)] sm:inline-block" title="Loyalty points" > - {points !== "—" ? `${points} pts` : "Stay"} + {`${points} pts`}