Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+| 9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
literal 0
HcmV?d00001
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..1eb12a3
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,72 @@
+@import "tailwindcss";
+
+:root {
+ --color-bg: #faf7f2;
+ --color-surface: #ffffff;
+ --color-surface-muted: #f3ede6;
+ --color-text: #1c1917;
+ --color-muted: #57534e;
+ --color-border: #e7e0d6;
+ --color-primary: #7c1d2b;
+ --color-primary-hover: #5c1520;
+ --color-on-primary: #fffaf7;
+ --color-accent: #b8860b;
+ --color-accent-soft: #f5e6c8;
+ --color-success: #0d9488;
+ --font-display: var(--font-cormorant), "Georgia", serif;
+ --font-ui: var(--font-dm-sans), system-ui, sans-serif;
+}
+
+@theme inline {
+ --color-background: var(--color-bg);
+ --color-foreground: var(--color-text);
+ --font-sans: var(--font-ui);
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ background: var(--color-bg);
+ color: var(--color-text);
+ font-family: var(--font-ui);
+}
+
+.font-display {
+ font-family: var(--font-display);
+}
+
+.grain::before {
+ content: "";
+ pointer-events: none;
+ position: fixed;
+ inset: 0;
+ opacity: 0.035;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
+ z-index: 50;
+}
+
+.card-lift {
+ transition:
+ transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
+ box-shadow 0.35s ease;
+}
+.card-lift:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 20px 40px rgba(28, 25, 23, 0.08);
+}
+
+@keyframes mock3d-rotate {
+ from {
+ transform: rotateY(-12deg) rotateX(4deg);
+ }
+ to {
+ transform: rotateY(12deg) rotateX(4deg);
+ }
+}
+
+.mock3d-plane {
+ animation: mock3d-rotate 8s ease-in-out infinite alternate;
+ transform-style: preserve-3d;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..29f2b81
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,47 @@
+import type { Metadata } from "next";
+import { Cormorant_Garamond, DM_Sans } from "next/font/google";
+import { Providers } from "./providers";
+import { Shell } from "@/components/Shell";
+import "./globals.css";
+
+const cormorant = Cormorant_Garamond({
+ variable: "--font-cormorant",
+ subsets: ["latin"],
+ weight: ["400", "500", "600", "700"],
+ display: "swap",
+});
+
+const dmSans = DM_Sans({
+ variable: "--font-dm-sans",
+ subsets: ["latin"],
+ weight: ["400", "500", "600", "700"],
+ display: "swap",
+});
+
+export const metadata: Metadata = {
+ title: {
+ default: "Shitaye Suite Hotel | Addis Ababa",
+ template: "%s | Shitaye Suite Hotel",
+ },
+ description:
+ "The Unwinding Choice — luxury suites, dining, and meetings in Addis Ababa. Book your stay at Shitaye Suite Hotel.",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/app/meetings/[slug]/not-found.tsx b/src/app/meetings/[slug]/not-found.tsx
new file mode 100644
index 0000000..15dc4f6
--- /dev/null
+++ b/src/app/meetings/[slug]/not-found.tsx
@@ -0,0 +1,15 @@
+import Link from "next/link";
+
+export default function MeetingNotFound() {
+ return (
+
+ Meeting space not found
+
+ View venues
+
+
+ );
+}
diff --git a/src/app/meetings/[slug]/page.tsx b/src/app/meetings/[slug]/page.tsx
new file mode 100644
index 0000000..9f22d6f
--- /dev/null
+++ b/src/app/meetings/[slug]/page.tsx
@@ -0,0 +1,153 @@
+import Image from "next/image";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { AmenityItem } from "@/components/AmenityItem";
+import { MeetingHalfDayRate } from "@/components/MeetingHalfDayRate";
+import { roomAmenities } from "@/lib/mocks/amenities";
+import {
+ getAllMeetingSlugs,
+ getMeetingSpaceBySlug,
+} from "@/lib/mocks/meetingSpaces";
+import { siteConfig } from "@/lib/mocks/site";
+import type { Metadata } from "next";
+
+type Props = { params: Promise<{ slug: string }> };
+
+export function generateStaticParams() {
+ return getAllMeetingSlugs().map((slug) => ({ slug }));
+}
+
+export async function generateMetadata({ params }: Props): Promise {
+ const { slug } = await params;
+ const m = getMeetingSpaceBySlug(slug);
+ if (!m) return { title: "Meeting space" };
+ return {
+ title: m.name,
+ description: m.shortDescription,
+ };
+}
+
+export default async function MeetingSpacePage({ params }: Props) {
+ const { slug } = await params;
+ const space = getMeetingSpaceBySlug(slug);
+ if (!space) notFound();
+
+ return (
+
+
+
+
+
+
+ ← Dining & venues
+
+ {space.name}
+ {space.shortDescription}
+
+
+
+
+
+
+ Overview
+ {space.longDescription}
+
+
+ {space.gallery.slice(1).map((src) => (
+
+
+
+ ))}
+
+
+
+ Amenities & equipment
+
+ {space.amenities.map((a) => (
+
+ ))}
+
+
+
+
+ Layouts
+
+ {space.layouts.map((l) => (
+ -
+ {l}
+
+ ))}
+
+
+
+
+ Catering
+
+ {space.catering.map((c) => (
+ - · {c}
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..0339991
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,270 @@
+import Image from "next/image";
+import Link from "next/link";
+import { AmenityItem } from "@/components/AmenityItem";
+import { BookingSearchWidget } from "@/components/BookingSearchWidget";
+import { OutletCard } from "@/components/OutletCard";
+import { RoomCard } from "@/components/RoomCard";
+import { VirtualTourBlock } from "@/components/VirtualTourBlock";
+import { roomAmenities } from "@/lib/mocks/amenities";
+import { outlets } from "@/lib/mocks/outlets";
+import { rooms } from "@/lib/mocks/rooms";
+import { siteConfig } from "@/lib/mocks/site";
+import { wellnessFacilities } from "@/lib/mocks/wellness";
+
+const heroImage =
+ "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1920&q=80";
+
+export default function HomePage() {
+ return (
+ <>
+
+
+
+
+
+ Official website
+
+
+ {siteConfig.tagline}
+
+
+ Discover refined stays in Addis Ababa — exceptional rooms, celebrated dining, and
+ spaces designed for connection.
+
+
+
+
+
+
+
+
+
+
+
+ About us
+
+
+ Geo-convenient. Unmistakably Shitaye.
+
+
+ Close to key places of attraction and major businesses or institutions — your base
+ for work, culture, and rest. Begin your journey with us.
+
+
+ View rooms
+
+
+
+
+
+
+
+
+
+
+
+ Stay with us
+
+ Rooms & suites
+
+ From junior studios to our four-bedroom penthouse — every category includes premium
+ amenities and attentive service.
+
+
+
+ Book a room →
+
+
+
+ {rooms.map((room) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {wellnessFacilities.map((w, i) => (
+ div:first-child]:order-2" : ""}`}
+ >
+
+
+
+
+
+ {w.subtitle}
+
+
+ {w.title}
+
+
+ {w.description}
+
+
+ {w.hours}
+
+
+ {w.amenities.map((a) => (
+
+ ))}
+
+
+
+ ))}
+
+
+
+
+
+
+ Explore in 3D
+
+ Virtual experience
+
+ Walk the property before you arrive — demo preview below; add a Matterport link in
+ config when ready.
+
+
+
+
+
+
+
+
+
+ Our outlets & services
+
+ Dining & venues
+
+ From FeastVille to TABSIA — savour flavour, host memorable events, and unwind in
+ spaces crafted for the city.
+
+
+ {outlets.map((o) => (
+
+ ))}
+
+
+
+
+
+
+ Meetings & celebrations
+
+ Serenity Meeting Room and Fasika Board Room — fully equipped for board sessions,
+ cocktails, and curated catering.
+
+
+
+ Serenity — details
+
+
+ Fasika — details
+
+
+
+ Plan an event
+
+
+
+
+
+
+ All rooms include
+
+ {roomAmenities.map((a) => (
+
+ ))}
+
+
+
+
+
+ Trusted stays. Seamless booking.
+
+ Reserve in minutes — mock checkout demonstrates the full journey.
+
+
+ Get started
+
+
+ >
+ );
+}
diff --git a/src/app/payment/PaymentPageClient.tsx b/src/app/payment/PaymentPageClient.tsx
new file mode 100644
index 0000000..02302c4
--- /dev/null
+++ b/src/app/payment/PaymentPageClient.tsx
@@ -0,0 +1,236 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { useBooking } from "@/context/BookingContext";
+import { useCurrency } from "@/context/CurrencyContext";
+import { siteConfig } from "@/lib/mocks/site";
+import { formatArrivalTimeDisplay } from "@/lib/formatArrivalTime";
+import { processPayment } from "@/lib/mocks/api";
+
+export function PaymentPageClient() {
+ const router = useRouter();
+ const {
+ selectedRoom,
+ guest,
+ nights,
+ subtotal,
+ taxAmount,
+ discountAmount,
+ total,
+ couponCode,
+ setCouponCode,
+ applyCoupon,
+ couponPercentOff,
+ holdReference,
+ payLaterHold,
+ setConfirmation,
+ } = useBooking();
+
+ const { formatUsd } = useCurrency();
+
+ const [cardName, setCardName] = useState("");
+ const [cardNumber, setCardNumber] = useState("");
+ const [expiry, setExpiry] = useState("");
+ const [cvv, setCvv] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ if (!selectedRoom || !guest.email) {
+ router.replace("/booking");
+ }
+ }, [selectedRoom, guest.email, router]);
+
+ if (!selectedRoom) {
+ return null;
+ }
+
+ const payLabel = `Confirm & pay ${formatUsd(total)}`;
+
+ async function handlePay() {
+ setLoading(true);
+ try {
+ const last4 = cardNumber.replace(/\D/g, "").slice(-4) || "0000";
+ const result = await processPayment({
+ totalCents: Math.round(total * 100),
+ last4,
+ });
+ setConfirmation(result.confirmationId, result.paidAt);
+ router.push("/confirmation");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ Payment
+
+ Mock form only — read our privacy policy before a real launch.
+
+
+ {payLaterHold ? (
+
+ You're completing a pay-later hold. Reference{" "}
+ {holdReference ?? "—"}.
+
+ ) : null}
+
+
+ Booking reference:{" "}
+
+ {holdReference ?? "—"}
+
+
+
+
+
+
+ Price details
+
+ setCouponCode(e.target.value)}
+ placeholder="Coupon code"
+ className="flex-1 rounded-xl border border-[var(--color-border)] px-3 py-2 text-sm"
+ />
+
+
+ {couponPercentOff > 0 ? (
+
+ {couponPercentOff}% discount applied (try SHITAYE10 or WELCOME5)
+
+ ) : null}
+
+
+
+ -
+ {formatUsd(selectedRoom.nightlyRate)} × {nights} nights
+
+ - {formatUsd(subtotal)}
+
+ {discountAmount > 0 ? (
+
+ - Discount
+ - -{formatUsd(discountAmount)}
+
+ ) : null}
+
+ - Taxes & fees ({siteConfig.taxRate * 100}%)
+ - {formatUsd(taxAmount)}
+
+
+ - Total
+ - {formatUsd(total)}
+
+
+
+
+
+
+
+
+
+ {selectedRoom.name}
+
+ {guest.firstName} {guest.lastName}
+
+
+
+
+
+
+ Flight arrival
+
+
+
+ - Booking / PNR
+ -
+ {guest.flightBookingNumber.trim() || "—"}
+
+
+
+ - Arrival time (local)
+ -
+ {formatArrivalTimeDisplay(guest.arrivalTime)}
+
+
+
+
+
+
+
+
+ Back to guest details
+
+
+ );
+}
diff --git a/src/app/payment/page.tsx b/src/app/payment/page.tsx
new file mode 100644
index 0000000..bc44083
--- /dev/null
+++ b/src/app/payment/page.tsx
@@ -0,0 +1,10 @@
+import { Suspense } from "react";
+import { PaymentPageClient } from "./PaymentPageClient";
+
+export default function PaymentPage() {
+ return (
+ Loading…}>
+
+
+ );
+}
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
new file mode 100644
index 0000000..92391de
--- /dev/null
+++ b/src/app/providers.tsx
@@ -0,0 +1,13 @@
+"use client";
+
+import { BookingProvider } from "@/context/BookingContext";
+import { CurrencyProvider } from "@/context/CurrencyContext";
+import type { ReactNode } from "react";
+
+export function Providers({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/reserve-held/page.tsx b/src/app/reserve-held/page.tsx
new file mode 100644
index 0000000..d6a8c5d
--- /dev/null
+++ b/src/app/reserve-held/page.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import { useBooking } from "@/context/BookingContext";
+import { useCurrency } from "@/context/CurrencyContext";
+import { formatArrivalTimeDisplay } from "@/lib/formatArrivalTime";
+import { siteConfig } from "@/lib/mocks/site";
+
+export default function ReserveHeldPage() {
+ const router = useRouter();
+ const {
+ holdReference,
+ selectedRoom,
+ guest,
+ checkIn,
+ checkOut,
+ nights,
+ total,
+ payLaterHold,
+ resetBooking,
+ } = useBooking();
+ const { formatUsd } = useCurrency();
+
+ useEffect(() => {
+ if (!holdReference || !selectedRoom) {
+ router.replace("/booking");
+ return;
+ }
+ if (!payLaterHold) {
+ router.replace("/payment");
+ }
+ }, [holdReference, selectedRoom, payLaterHold, router]);
+
+ if (!holdReference || !selectedRoom || !payLaterHold) {
+ return null;
+ }
+
+ return (
+
+
+
+ Reservation on hold
+
+
+ {guest.firstName}, your room is saved — finish payment whenever you're ready in this
+ browser session. (Demo: no real hold or email.)
+
+
+ Hold ref: {holdReference}
+
+
+ Indicative total when you pay:{" "}
+ {formatUsd(total)}
+
+
+
+
+
+
+
+ {siteConfig.name}
+ {selectedRoom.name}
+
+ {checkIn} → {checkOut} · {nights} night{nights !== 1 ? "s" : ""}
+
+
+ Flight {guest.flightBookingNumber.trim()} · arrival{" "}
+ {formatArrivalTimeDisplay(guest.arrivalTime)}
+
+
+
+
+
+ Complete payment
+
+
+ resetBooking()}
+ className="mt-4 block text-center text-sm font-medium text-[var(--color-primary)] hover:underline"
+ >
+ Back to home — discard this hold
+
+
+ );
+}
diff --git a/src/app/rooms/[slug]/not-found.tsx b/src/app/rooms/[slug]/not-found.tsx
new file mode 100644
index 0000000..3a2a832
--- /dev/null
+++ b/src/app/rooms/[slug]/not-found.tsx
@@ -0,0 +1,18 @@
+import Link from "next/link";
+
+export default function RoomNotFound() {
+ return (
+
+ Room not found
+
+ We couldn't find that room category.
+
+
+ View all rooms
+
+
+ );
+}
diff --git a/src/app/rooms/[slug]/page.tsx b/src/app/rooms/[slug]/page.tsx
new file mode 100644
index 0000000..e486c91
--- /dev/null
+++ b/src/app/rooms/[slug]/page.tsx
@@ -0,0 +1,159 @@
+import Image from "next/image";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { AmenityItem } from "@/components/AmenityItem";
+import { FormattedUsd } from "@/components/FormattedUsd";
+import { BookRoomButton } from "@/components/BookRoomButton";
+import { VirtualTourBlock } from "@/components/VirtualTourBlock";
+import { roomAmenities } from "@/lib/mocks/amenities";
+import { getAllRoomSlugs, getRoomBySlug } from "@/lib/mocks/rooms";
+import { siteConfig } from "@/lib/mocks/site";
+import type { Metadata } from "next";
+
+type Props = { params: Promise<{ slug: string }> };
+
+export function generateStaticParams() {
+ return getAllRoomSlugs().map((slug) => ({ slug }));
+}
+
+export async function generateMetadata({ params }: Props): Promise {
+ const { slug } = await params;
+ const room = getRoomBySlug(slug);
+ if (!room) return { title: "Room" };
+ return {
+ title: room.name,
+ description: room.shortDescription,
+ };
+}
+
+export default async function RoomPage({ params }: Props) {
+ const { slug } = await params;
+ const room = getRoomBySlug(slug);
+ if (!room) notFound();
+
+ return (
+
+
+
+
+
+
+ ← All rooms
+
+ {room.name}
+ {room.shortDescription}
+
+
+
+
+
+
+ Overview
+ {room.longDescription}
+
+
+ {room.gallery.slice(1).map((src) => (
+
+
+
+ ))}
+
+
+
+ Virtual tour
+
+ Explore this category in 3D — demo placeholder until a room-specific embed is
+ added.
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/AmenityItem.tsx b/src/components/AmenityItem.tsx
new file mode 100644
index 0000000..4921f1a
--- /dev/null
+++ b/src/components/AmenityItem.tsx
@@ -0,0 +1,48 @@
+import { AmenityIcon } from "@/components/icons/AmenityIcon";
+import type { AmenityWithIcon } from "@/lib/mocks/amenities";
+
+type Props = {
+ item: AmenityWithIcon;
+ variant?: "card" | "inline";
+ /** Use on tinted bands (e.g. “All rooms include”) so cards stay readable */
+ cardTone?: "elevated" | "embedded";
+};
+
+export function AmenityItem({
+ item,
+ variant = "card",
+ cardTone = "elevated",
+}: Props) {
+ if (variant === "inline") {
+ return (
+
+
+
+
+ {item.label}
+
+ );
+ }
+
+ const cardBg =
+ cardTone === "embedded"
+ ? "bg-[var(--color-bg)]"
+ : "bg-[var(--color-surface)]";
+
+ return (
+
+
+
+
+ {item.label}
+
+ );
+}
diff --git a/src/components/BookRoomButton.tsx b/src/components/BookRoomButton.tsx
new file mode 100644
index 0000000..a71e7bd
--- /dev/null
+++ b/src/components/BookRoomButton.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useBooking } from "@/context/BookingContext";
+
+type Props = { roomId: string; className?: string };
+
+export function BookRoomButton({ roomId, className = "" }: Props) {
+ const { setRoomId } = useBooking();
+ const router = useRouter();
+
+ return (
+
+ );
+}
diff --git a/src/components/BookingSearchWidget.tsx b/src/components/BookingSearchWidget.tsx
new file mode 100644
index 0000000..990749a
--- /dev/null
+++ b/src/components/BookingSearchWidget.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useBooking } from "@/context/BookingContext";
+import { useCallback } from "react";
+
+export function BookingSearchWidget() {
+ const { checkIn, checkOut, guests, setDates, setGuests } = useBooking();
+ const router = useRouter();
+
+ const onSearch = useCallback(() => {
+ router.push("/booking");
+ }, [router]);
+
+ return (
+
+
+ Begin your journey
+
+
+
+ {["Suites", "Studios", "Penthouse", "Meetings"].map((chip) => (
+
+ {chip}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/CallUsFab.tsx b/src/components/CallUsFab.tsx
new file mode 100644
index 0000000..8dec210
--- /dev/null
+++ b/src/components/CallUsFab.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { siteConfig } from "@/lib/mocks/site";
+
+export function CallUsFab() {
+ const tel = siteConfig.primaryPhone.replace(/\s/g, "");
+
+ return (
+
+ );
+}
+
+function MailIcon() {
+ return (
+
+ );
+}
+
+function PhoneIcon() {
+ return (
+
+ );
+}
diff --git a/src/components/CurrencySwitcher.tsx b/src/components/CurrencySwitcher.tsx
new file mode 100644
index 0000000..a184684
--- /dev/null
+++ b/src/components/CurrencySwitcher.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import { useCurrency } from "@/context/CurrencyContext";
+import { CURRENCY_OPTIONS, type CurrencyCode } from "@/lib/currency";
+
+export function CurrencySwitcher() {
+ const { currency, setCurrency } = useCurrency();
+
+ return (
+
+ );
+}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000..07c72ad
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,185 @@
+import Link from "next/link";
+import { siteConfig } from "@/lib/mocks/site";
+
+export function Footer() {
+ return (
+
+ );
+}
+
+function IconFacebook() {
+ return (
+
+ );
+}
+
+function IconInstagram() {
+ return (
+
+ );
+}
+
+function IconTwitter() {
+ return (
+
+ );
+}
+
+function IconWhatsApp() {
+ return (
+
+ );
+}
diff --git a/src/components/FormattedUsd.tsx b/src/components/FormattedUsd.tsx
new file mode 100644
index 0000000..3ed5db4
--- /dev/null
+++ b/src/components/FormattedUsd.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { useCurrency } from "@/context/CurrencyContext";
+
+type Props = {
+ amountUsd: number;
+ /** @default 2 */
+ maximumFractionDigits?: 0 | 1 | 2;
+ className?: string;
+};
+
+export function FormattedUsd({
+ amountUsd,
+ maximumFractionDigits = 2,
+ className,
+}: Props) {
+ const { formatUsd } = useCurrency();
+ return (
+ {formatUsd(amountUsd, maximumFractionDigits)}
+ );
+}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 0000000..9abebc8
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,67 @@
+import Link from "next/link";
+import { CurrencySwitcher } from "@/components/CurrencySwitcher";
+import { ReviewsMenu } from "@/components/ReviewsMenu";
+import { siteConfig } from "@/lib/mocks/site";
+
+const nav = [
+ { href: "/#rooms", label: "Rooms" },
+ { href: "/#wellness", label: "Gym & Spa" },
+ { href: "/#dining", label: "Dining & venues" },
+ { href: "/#meetings", label: "Meetings" },
+];
+
+export function Header() {
+ return (
+
+
+
+
+
+
+ 3D tour
+
+
+
+
+
+
+
+
+
+
+ {siteConfig.name}
+
+
+ {siteConfig.city}
+
+
+
+
+
+ Book
+
+
+
+
+
+ );
+}
diff --git a/src/components/MeetingHalfDayRate.tsx b/src/components/MeetingHalfDayRate.tsx
new file mode 100644
index 0000000..434a8b9
--- /dev/null
+++ b/src/components/MeetingHalfDayRate.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { useCurrency } from "@/context/CurrencyContext";
+
+type Props = { usdAmount: number };
+
+export function MeetingHalfDayRate({ usdAmount }: Props) {
+ const { formatUsd } = useCurrency();
+ return (
+
+ From {formatUsd(usdAmount)} / half day (indicative — enquire)
+
+ );
+}
diff --git a/src/components/Mock3DPlaceholder.tsx b/src/components/Mock3DPlaceholder.tsx
new file mode 100644
index 0000000..8ba17b8
--- /dev/null
+++ b/src/components/Mock3DPlaceholder.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { useCallback, useRef, useState } from "react";
+
+type Props = {
+ label?: string;
+ videoTourUrl?: string | null;
+ className?: string;
+};
+
+export function Mock3DPlaceholder({
+ label = "3D preview (demo)",
+ videoTourUrl,
+ className = "",
+}: Props) {
+ const [tilt, setTilt] = useState({ x: 4, y: 0 });
+ const dragging = useRef(false);
+ const last = useRef({ x: 0, y: 0 });
+
+ const onPointerDown = (e: React.PointerEvent) => {
+ dragging.current = true;
+ last.current = { x: e.clientX, y: e.clientY };
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
+ };
+
+ const onPointerMove = useCallback((e: React.PointerEvent) => {
+ if (!dragging.current) return;
+ const dx = e.clientX - last.current.x;
+ const dy = e.clientY - last.current.y;
+ last.current = { x: e.clientX, y: e.clientY };
+ setTilt((t) => ({
+ x: Math.max(-8, Math.min(14, t.x - dy * 0.08)),
+ y: Math.max(-18, Math.min(18, t.y + dx * 0.12)),
+ }));
+ }, []);
+
+ const onPointerUp = (e: React.PointerEvent) => {
+ dragging.current = false;
+ try {
+ (e.target as HTMLElement).releasePointerCapture(e.pointerId);
+ } catch {
+ /* ignore */
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ Shitaye
+
+ Virtual walkthrough
+
+
+
+
+
+ );
+}
diff --git a/src/components/OutletCard.tsx b/src/components/OutletCard.tsx
new file mode 100644
index 0000000..6a1a66d
--- /dev/null
+++ b/src/components/OutletCard.tsx
@@ -0,0 +1,63 @@
+import Image from "next/image";
+import Link from "next/link";
+import type { Outlet } from "@/lib/mocks/outlets";
+
+type Props = { outlet: Outlet };
+
+export function OutletCard({ outlet }: Props) {
+ const inner = (
+ <>
+
+
+
+
+ {outlet.floor ? (
+
+ {outlet.floor}
+
+ ) : null}
+ {outlet.name}
+ {outlet.tagline}
+ {outlet.detailHref ? (
+
+ View details →
+
+ ) : null}
+
+
+
+ {outlet.bullets.map((b) => (
+ -
+
+ ·
+
+ {b}
+
+ ))}
+
+ >
+ );
+
+ if (outlet.detailHref) {
+ return (
+
+ {inner}
+
+ );
+ }
+
+ return (
+
+ {inner}
+
+ );
+}
diff --git a/src/components/ReviewsMenu.tsx b/src/components/ReviewsMenu.tsx
new file mode 100644
index 0000000..1116175
--- /dev/null
+++ b/src/components/ReviewsMenu.tsx
@@ -0,0 +1,325 @@
+"use client";
+
+import Link from "next/link";
+import {
+ useCallback,
+ useEffect,
+ useId,
+ useRef,
+ useState,
+ useSyncExternalStore,
+} from "react";
+import { createPortal } from "react-dom";
+import {
+ bookingStyleReviews,
+ overallRatingOutOfFive,
+} from "@/lib/mocks/bookingReviews";
+import { siteConfig } from "@/lib/mocks/site";
+
+function useIsClient() {
+ return useSyncExternalStore(
+ () => () => {},
+ () => true,
+ () => false,
+ );
+}
+
+type ReviewsMenuProps = { variant?: "default" | "topBar" };
+
+export function ReviewsMenu({ variant = "default" }: ReviewsMenuProps) {
+ const [open, setOpen] = useState(false);
+ const isTopBar = variant === "topBar";
+ const mounted = useIsClient();
+ const triggerRef = useRef(null);
+ const panelRef = useRef(null);
+
+ const close = useCallback(() => setOpen(false), []);
+
+ useEffect(() => {
+ if (!open) return;
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ const triggerEl = triggerRef.current;
+
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") close();
+ }
+ document.addEventListener("keydown", onKey);
+ queueMicrotask(() => {
+ panelRef.current
+ ?.querySelector("button[aria-label='Close']")
+ ?.focus();
+ });
+
+ return () => {
+ document.body.style.overflow = prev;
+ document.removeEventListener("keydown", onKey);
+ triggerEl?.focus();
+ };
+ }, [open, close]);
+
+ const dialog =
+ open && mounted ? (
+
+
+ {/* min-h-full = full viewport inside fixed inset-0; items-center centers the card */}
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ Guest reviews
+
+
+ Sample scores for layout — connect your live Booking.com page when ready.
+
+
+
+
+ {overallRatingOutOfFive}
+
+ {" "}
+ / 5
+
+
+
+
+
+
+
+
+ {bookingStyleReviews.map((r) => (
+ -
+
+
+ {r.author}
+
+ {r.country} · {r.stayDate}
+
+
+
+ {r.rating}
+
+
+ {r.title}
+
+ {r.text}
+
+ Stayed in: {r.roomType}
+
+ ))}
+
+
+
+
+
+ Read all reviews
+
+
+
+
+
+ ) : null;
+
+ return (
+ <>
+
+
+ {mounted && dialog ? createPortal(dialog, document.body) : null}
+ >
+ );
+}
+
+/** Booking.com–style wordmark: “Booking” + yellow dot + “.com” */
+function BookingDotLogo({
+ className = "",
+ compact = false,
+}: {
+ className?: string;
+ compact?: boolean;
+}) {
+ if (compact) {
+ return (
+
+ Booking
+
+ .com
+
+ );
+ }
+ return (
+
+ Booking
+
+ .com
+
+ );
+}
+
+/** Five circles: filled amount per position = min(1, max(0, rating - i)) */
+function CircleRatingRow({ rating, max = 5 }: { rating: number; max?: number }) {
+ const uid = useId().replace(/:/g, "");
+ const size = 26;
+ const vb = 24;
+ const cx = 12;
+ const cy = 12;
+ const r = 8;
+ const stroke = "#d6d3d1";
+
+ return (
+
+ {Array.from({ length: max }, (_, i) => {
+ const fill = Math.min(1, Math.max(0, rating - i));
+ const clipW = vb * fill;
+ const clipId = `${uid}-clip-${i}`;
+ return (
+
+ );
+ })}
+
+ );
+}
+
+function CircleRatingMini({ rating }: { rating: number }) {
+ const uid = useId().replace(/:/g, "");
+ const max = 5;
+ const vb = 24;
+ const cx = 12;
+ const cy = 12;
+ const r = 7;
+
+ return (
+
+ {Array.from({ length: max }, (_, i) => {
+ const fill = Math.min(1, Math.max(0, rating - i));
+ const clipW = vb * fill;
+ const clipId = `${uid}-m-${i}`;
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/RoomCard.tsx b/src/components/RoomCard.tsx
new file mode 100644
index 0000000..bbe84af
--- /dev/null
+++ b/src/components/RoomCard.tsx
@@ -0,0 +1,53 @@
+import Image from "next/image";
+import Link from "next/link";
+import { FormattedUsd } from "@/components/FormattedUsd";
+import type { Room } from "@/lib/mocks/rooms";
+
+type Props = { room: Room };
+
+export function RoomCard({ room }: Props) {
+ return (
+
+
+
+
+ From
+ / night
+
+
+
+
+
+ {room.name}
+
+
+
+ {room.shortDescription}
+
+
+
+ View details
+
+
+
+ →
+
+
+
+
+
+ );
+}
diff --git a/src/components/RoomSelectBooking.tsx b/src/components/RoomSelectBooking.tsx
new file mode 100644
index 0000000..d2adc1d
--- /dev/null
+++ b/src/components/RoomSelectBooking.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { useEffect, useRef, useState } from "react";
+import { FormattedUsd } from "@/components/FormattedUsd";
+import type { Room } from "@/lib/mocks/rooms";
+import { rooms } from "@/lib/mocks/rooms";
+
+type Props = {
+ selected: Room | null;
+ onSelect: (roomId: string) => void;
+};
+
+export function RoomSelectBooking({ selected, onSelect }: Props) {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ function onDoc(e: MouseEvent) {
+ if (!ref.current?.contains(e.target as Node)) setOpen(false);
+ }
+ if (open) document.addEventListener("mousedown", onDoc);
+ return () => document.removeEventListener("mousedown", onDoc);
+ }, [open]);
+
+ return (
+
+
+ Select room
+
+
+
+ {open ? (
+
+ {rooms.map((room) => (
+ -
+
+
+ ))}
+
+ ) : null}
+
+ {selected ? (
+
+ View full room details & amenities
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/Shell.tsx b/src/components/Shell.tsx
new file mode 100644
index 0000000..9ad955e
--- /dev/null
+++ b/src/components/Shell.tsx
@@ -0,0 +1,14 @@
+import { CallUsFab } from "./CallUsFab";
+import { Footer } from "./Footer";
+import { Header } from "./Header";
+
+export function Shell({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
diff --git a/src/components/VirtualTourBlock.tsx b/src/components/VirtualTourBlock.tsx
new file mode 100644
index 0000000..2eea01b
--- /dev/null
+++ b/src/components/VirtualTourBlock.tsx
@@ -0,0 +1,28 @@
+import { siteConfig } from "@/lib/mocks/site";
+import { Mock3DPlaceholder } from "./Mock3DPlaceholder";
+import { VirtualTourEmbed } from "./VirtualTourEmbed";
+
+type Props = {
+ embedUrl: string | null | undefined;
+ title: string;
+ videoTourUrl?: string | null;
+ className?: string;
+};
+
+export function VirtualTourBlock({
+ embedUrl,
+ title,
+ videoTourUrl = siteConfig.videoTourUrl,
+ className,
+}: Props) {
+ if (embedUrl) {
+ return ;
+ }
+ return (
+
+ );
+}
diff --git a/src/components/VirtualTourEmbed.tsx b/src/components/VirtualTourEmbed.tsx
new file mode 100644
index 0000000..f5e7db7
--- /dev/null
+++ b/src/components/VirtualTourEmbed.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+type Props = {
+ src: string;
+ title: string;
+ className?: string;
+};
+
+export function VirtualTourEmbed({ src, title, className = "" }: Props) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/icons/AmenityIcon.tsx b/src/components/icons/AmenityIcon.tsx
new file mode 100644
index 0000000..0eacff2
--- /dev/null
+++ b/src/components/icons/AmenityIcon.tsx
@@ -0,0 +1,363 @@
+import type { ReactNode, SVGProps } from "react";
+
+export type AmenityIconId =
+ | "breakfast"
+ | "shuttle"
+ | "wifi"
+ | "sparkle"
+ | "tv"
+ | "kitchen"
+ | "views"
+ | "minibar"
+ | "lock"
+ | "iron"
+ | "router"
+ | "laundry"
+ | "projector"
+ | "microphone"
+ | "clipboard"
+ | "thermometer"
+ | "handshake"
+ | "doorOpen"
+ | "accessibility"
+ | "monitor"
+ | "video"
+ | "chair"
+ | "volumeMuted"
+ | "restroom"
+ | "pen"
+ | "droplet"
+ | "phone"
+ | "treadmill"
+ | "dumbbell"
+ | "stretch"
+ | "towel"
+ | "headphones"
+ | "massage"
+ | "steam"
+ | "leaf"
+ | "lounge"
+ | "boutique";
+
+type IconProps = SVGProps;
+
+function Shell({ className, children, ...rest }: IconProps & { children: ReactNode }) {
+ return (
+
+ );
+}
+
+const stroke = {
+ stroke: "currentColor",
+ strokeWidth: 1.5,
+ strokeLinecap: "round" as const,
+ strokeLinejoin: "round" as const,
+};
+
+/**
+ * Brand-colored amenity glyphs (use with `className="text-[var(--color-primary)]"` or accent).
+ */
+export function AmenityIcon({ id, className }: { id: AmenityIconId; className?: string }) {
+ switch (id) {
+ case "breakfast":
+ return (
+
+
+
+
+
+ );
+ case "shuttle":
+ return (
+
+
+
+
+
+
+
+ );
+ case "wifi":
+ return (
+
+
+
+
+
+ );
+ case "sparkle":
+ return (
+
+
+
+
+ );
+ case "tv":
+ return (
+
+
+
+
+ );
+ case "kitchen":
+ return (
+
+
+
+
+
+ );
+ case "views":
+ return (
+
+
+
+
+
+ );
+ case "minibar":
+ return (
+
+
+
+
+
+ );
+ case "lock":
+ return (
+
+
+
+
+ );
+ case "iron":
+ return (
+
+
+
+
+
+ );
+ case "router":
+ return (
+
+
+
+
+
+
+ );
+ case "laundry":
+ return (
+
+
+
+
+
+ );
+ case "projector":
+ return (
+
+
+
+
+
+ );
+ case "microphone":
+ return (
+
+
+
+
+ );
+ case "clipboard":
+ return (
+
+
+
+
+ );
+ case "thermometer":
+ return (
+
+
+
+
+ );
+ case "handshake":
+ return (
+
+
+
+
+ );
+ case "doorOpen":
+ return (
+
+
+
+
+
+ );
+ case "accessibility":
+ return (
+
+
+
+
+ );
+ case "monitor":
+ return (
+
+
+
+
+ );
+ case "video":
+ return (
+
+
+
+
+ );
+ case "chair":
+ return (
+
+
+
+
+
+ );
+ case "volumeMuted":
+ return (
+
+
+
+
+ );
+ case "restroom":
+ return (
+
+
+
+
+
+
+ );
+ case "pen":
+ return (
+
+
+
+
+ );
+ case "droplet":
+ return (
+
+
+
+ );
+ case "phone":
+ return (
+
+
+
+ );
+ case "treadmill":
+ return (
+
+
+
+
+
+
+ );
+ case "dumbbell":
+ return (
+
+
+
+
+
+ );
+ case "stretch":
+ return (
+
+
+
+
+
+ );
+ case "towel":
+ return (
+
+
+
+ );
+ case "headphones":
+ return (
+
+
+
+
+ );
+ case "massage":
+ return (
+
+
+
+
+ );
+ case "steam":
+ return (
+
+
+
+
+ );
+ case "leaf":
+ return (
+
+
+
+
+ );
+ case "lounge":
+ return (
+
+
+
+
+ );
+ case "boutique":
+ return (
+
+
+
+
+
+ );
+ default:
+ return (
+
+
+
+ );
+ }
+}
diff --git a/src/context/BookingContext.tsx b/src/context/BookingContext.tsx
new file mode 100644
index 0000000..4287e61
--- /dev/null
+++ b/src/context/BookingContext.tsx
@@ -0,0 +1,218 @@
+"use client";
+
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import type { Room } from "@/lib/mocks/rooms";
+import { rooms } from "@/lib/mocks/rooms";
+import { siteConfig } from "@/lib/mocks/site";
+
+export type GuestDetails = {
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone: string;
+ /** Airline / PNR / booking reference */
+ flightBookingNumber: string;
+ /** Local arrival time (24h from time input) */
+ arrivalTime: string;
+};
+
+const defaultDates = () => {
+ const inD = new Date();
+ inD.setDate(inD.getDate() + 7);
+ const outD = new Date(inD);
+ outD.setDate(outD.getDate() + 3);
+ return {
+ checkIn: inD.toISOString().slice(0, 10),
+ checkOut: outD.toISOString().slice(0, 10),
+ };
+};
+
+function nightsBetween(checkIn: string, checkOut: string): number {
+ const a = new Date(checkIn).getTime();
+ const b = new Date(checkOut).getTime();
+ const n = Math.ceil((b - a) / (1000 * 60 * 60 * 24));
+ return Math.max(1, n);
+}
+
+type BookingContextValue = {
+ checkIn: string;
+ checkOut: string;
+ guests: number;
+ roomId: string | null;
+ guest: GuestDetails;
+ couponCode: string;
+ couponPercentOff: number;
+ holdReference: string | null;
+ /** True when guest chose “reserve now, pay later” (hold without payment yet) */
+ payLaterHold: boolean;
+ confirmationId: string | null;
+ paidAt: string | null;
+ setDates: (checkIn: string, checkOut: string) => void;
+ setGuests: (n: number) => void;
+ setRoomId: (id: string | null) => void;
+ setGuest: (g: Partial) => void;
+ setCouponCode: (code: string) => void;
+ applyCoupon: () => void;
+ setHoldReference: (ref: string | null) => void;
+ setPayLaterHold: (value: boolean) => void;
+ setConfirmation: (id: string | null, paidAt: string | null) => void;
+ resetBooking: () => void;
+ selectedRoom: Room | null;
+ nights: number;
+ subtotal: number;
+ taxAmount: number;
+ discountAmount: number;
+ total: number;
+};
+
+const BookingContext = createContext(null);
+
+const emptyGuest: GuestDetails = {
+ firstName: "",
+ lastName: "",
+ email: "",
+ phone: "",
+ flightBookingNumber: "",
+ arrivalTime: "",
+};
+
+export function BookingProvider({ children }: { children: ReactNode }) {
+ const d = defaultDates();
+ const [checkIn, setCheckIn] = useState(d.checkIn);
+ const [checkOut, setCheckOut] = useState(d.checkOut);
+ const [guests, setGuestsState] = useState(2);
+ const [roomId, setRoomIdState] = useState(null);
+ const [guest, setGuestState] = useState({ ...emptyGuest });
+ const [couponCode, setCouponCodeState] = useState("");
+ const [couponPercentOff, setCouponPercentOff] = useState(0);
+ const [holdReference, setHoldReference] = useState(null);
+ const [payLaterHold, setPayLaterHoldState] = useState(false);
+ const [confirmationId, setConfirmationId] = useState(null);
+ const [paidAt, setPaidAt] = useState(null);
+
+ const setDates = useCallback((ci: string, co: string) => {
+ setCheckIn(ci);
+ setCheckOut(co);
+ }, []);
+
+ const setGuests = useCallback((n: number) => {
+ setGuestsState(Math.min(12, Math.max(1, n)));
+ }, []);
+
+ const setRoomId = useCallback((id: string | null) => {
+ setRoomIdState(id);
+ }, []);
+
+ const setGuest = useCallback((g: Partial) => {
+ setGuestState((prev) => ({ ...prev, ...g }));
+ }, []);
+
+ const setCouponCode = useCallback((code: string) => {
+ setCouponCodeState(code);
+ setCouponPercentOff(0);
+ }, []);
+
+ const applyCoupon = useCallback(() => {
+ const c = couponCode.trim().toUpperCase();
+ if (c === "SHITAYE10") setCouponPercentOff(10);
+ else if (c === "WELCOME5") setCouponPercentOff(5);
+ else setCouponPercentOff(0);
+ }, [couponCode]);
+
+ const setPayLaterHold = useCallback((value: boolean) => {
+ setPayLaterHoldState(value);
+ }, []);
+
+ const setConfirmation = useCallback((id: string | null, at: string | null) => {
+ setConfirmationId(id);
+ setPaidAt(at);
+ if (id) setPayLaterHoldState(false);
+ }, []);
+
+ const resetBooking = useCallback(() => {
+ const nd = defaultDates();
+ setCheckIn(nd.checkIn);
+ setCheckOut(nd.checkOut);
+ setGuestsState(2);
+ setRoomIdState(null);
+ setGuestState({ ...emptyGuest });
+ setCouponCodeState("");
+ setCouponPercentOff(0);
+ setHoldReference(null);
+ setPayLaterHoldState(false);
+ setConfirmationId(null);
+ setPaidAt(null);
+ }, []);
+
+ const selectedRoom = useMemo(
+ () => rooms.find((r) => r.id === roomId) ?? null,
+ [roomId],
+ );
+
+ const nights = useMemo(
+ () => nightsBetween(checkIn, checkOut),
+ [checkIn, checkOut],
+ );
+
+ const subtotal = useMemo(() => {
+ if (!selectedRoom) return 0;
+ return selectedRoom.nightlyRate * nights;
+ }, [selectedRoom, nights]);
+
+ const discountAmount = useMemo(
+ () => Math.round(subtotal * (couponPercentOff / 100) * 100) / 100,
+ [subtotal, couponPercentOff],
+ );
+
+ const afterDiscount = Math.max(0, subtotal - discountAmount);
+ const taxAmount =
+ Math.round(afterDiscount * siteConfig.taxRate * 100) / 100;
+ const total = Math.round((afterDiscount + taxAmount) * 100) / 100;
+
+ const value: BookingContextValue = {
+ checkIn,
+ checkOut,
+ guests,
+ roomId,
+ guest,
+ couponCode,
+ couponPercentOff,
+ holdReference,
+ payLaterHold,
+ confirmationId,
+ paidAt,
+ setDates,
+ setGuests,
+ setRoomId,
+ setGuest,
+ setCouponCode,
+ applyCoupon,
+ setHoldReference,
+ setPayLaterHold,
+ setConfirmation,
+ resetBooking,
+ selectedRoom,
+ nights,
+ subtotal,
+ taxAmount,
+ discountAmount,
+ total,
+ };
+
+ return (
+ {children}
+ );
+}
+
+export function useBooking() {
+ const ctx = useContext(BookingContext);
+ if (!ctx) throw new Error("useBooking must be used within BookingProvider");
+ return ctx;
+}
diff --git a/src/context/CurrencyContext.tsx b/src/context/CurrencyContext.tsx
new file mode 100644
index 0000000..54c80dd
--- /dev/null
+++ b/src/context/CurrencyContext.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useSyncExternalStore,
+ type ReactNode,
+} from "react";
+import {
+ type CurrencyCode,
+ convertFromUsd,
+ formatMoneyFromUsd,
+ isCurrencyCode,
+} from "@/lib/currency";
+
+const STORAGE_KEY = "shitaye-currency";
+const CURRENCY_EVENT = "shitaye-currency-change";
+
+type CurrencyContextValue = {
+ currency: CurrencyCode;
+ setCurrency: (c: CurrencyCode) => void;
+ formatUsd: (amountUsd: number, maximumFractionDigits?: 0 | 1 | 2) => string;
+ convertUsd: (amountUsd: number) => number;
+};
+
+const CurrencyContext = createContext(null);
+
+function readCurrency(): CurrencyCode {
+ if (typeof window === "undefined") return "USD";
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw && isCurrencyCode(raw)) return raw;
+ } catch {
+ /* ignore */
+ }
+ return "USD";
+}
+
+function subscribe(onChange: () => void) {
+ if (typeof window === "undefined") return () => {};
+ const handler = () => onChange();
+ window.addEventListener("storage", handler);
+ window.addEventListener(CURRENCY_EVENT, handler);
+ return () => {
+ window.removeEventListener("storage", handler);
+ window.removeEventListener(CURRENCY_EVENT, handler);
+ };
+}
+
+export function CurrencyProvider({ children }: { children: ReactNode }) {
+ const currency = useSyncExternalStore(
+ subscribe,
+ readCurrency,
+ () => "USD" as CurrencyCode,
+ ) as CurrencyCode;
+
+ const setCurrency = useCallback((c: CurrencyCode) => {
+ try {
+ localStorage.setItem(STORAGE_KEY, c);
+ window.dispatchEvent(new Event(CURRENCY_EVENT));
+ } catch {
+ /* ignore */
+ }
+ }, []);
+
+ const value = useMemo((): CurrencyContextValue => {
+ return {
+ currency,
+ setCurrency,
+ formatUsd: (amountUsd, maximumFractionDigits = 2) =>
+ formatMoneyFromUsd(amountUsd, currency, maximumFractionDigits),
+ convertUsd: (amountUsd) => convertFromUsd(amountUsd, currency),
+ };
+ }, [currency, setCurrency]);
+
+ return (
+ {children}
+ );
+}
+
+export function useCurrency() {
+ const ctx = useContext(CurrencyContext);
+ if (!ctx) throw new Error("useCurrency must be used within CurrencyProvider");
+ return ctx;
+}
diff --git a/src/lib/currency.ts b/src/lib/currency.ts
new file mode 100644
index 0000000..d75aa98
--- /dev/null
+++ b/src/lib/currency.ts
@@ -0,0 +1,39 @@
+export type CurrencyCode = "USD" | "EUR" | "GBP" | "AED";
+
+export const CURRENCY_OPTIONS: { code: CurrencyCode; shortLabel: string }[] = [
+ { code: "USD", shortLabel: "USD" },
+ { code: "EUR", shortLabel: "EUR" },
+ { code: "GBP", shortLabel: "GBP" },
+ { code: "AED", shortLabel: "AED" },
+];
+
+/** Display amount = catalog USD × rate (illustrative mock rates). */
+export const USD_TO: Record = {
+ USD: 1,
+ EUR: 0.93,
+ GBP: 0.79,
+ AED: 3.67,
+};
+
+export function isCurrencyCode(v: string): v is CurrencyCode {
+ return v === "USD" || v === "EUR" || v === "GBP" || v === "AED";
+}
+
+export function convertFromUsd(usd: number, code: CurrencyCode): number {
+ const n = usd * USD_TO[code];
+ return Math.round(n * 100) / 100;
+}
+
+export function formatMoneyFromUsd(
+ usd: number,
+ code: CurrencyCode,
+ maximumFractionDigits: 0 | 1 | 2 = 2,
+): string {
+ const amount = convertFromUsd(usd, code);
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: code,
+ minimumFractionDigits: 0,
+ maximumFractionDigits,
+ }).format(amount);
+}
diff --git a/src/lib/formatArrivalTime.ts b/src/lib/formatArrivalTime.ts
new file mode 100644
index 0000000..d330e76
--- /dev/null
+++ b/src/lib/formatArrivalTime.ts
@@ -0,0 +1,11 @@
+/** Pretty-print a value from `` (HH:mm). */
+export function formatArrivalTimeDisplay(time24: string): string {
+ const t = time24.trim();
+ if (!t) return "—";
+ const parts = t.split(":");
+ const h = Number.parseInt(parts[0] ?? "", 10);
+ const m = Number.parseInt(parts[1] ?? "", 10);
+ if (Number.isNaN(h) || Number.isNaN(m)) return t;
+ const d = new Date(1970, 0, 1, h, m, 0, 0);
+ return d.toLocaleTimeString("en-GB", { hour: "numeric", minute: "2-digit" });
+}
diff --git a/src/lib/mocks/amenities.ts b/src/lib/mocks/amenities.ts
new file mode 100644
index 0000000..91ed432
--- /dev/null
+++ b/src/lib/mocks/amenities.ts
@@ -0,0 +1,21 @@
+import type { AmenityIconId } from "@/components/icons/AmenityIcon";
+
+export type AmenityWithIcon = {
+ icon: AmenityIconId;
+ label: string;
+};
+
+export const roomAmenities: AmenityWithIcon[] = [
+ { icon: "breakfast", label: "B/B Fast" },
+ { icon: "shuttle", label: "Shuttle" },
+ { icon: "wifi", label: "Wi‑Fi / LAN" },
+ { icon: "sparkle", label: "Premium amenities" },
+ { icon: "tv", label: "IPTV" },
+ { icon: "kitchen", label: "State of the art kitchenette" },
+ { icon: "views", label: "Amazing views" },
+ { icon: "minibar", label: "Mini bar" },
+ { icon: "lock", label: "Safe boxes" },
+ { icon: "iron", label: "Iron & board" },
+ { icon: "router", label: "Private routers" },
+ { icon: "laundry", label: "Laundry (paid services)" },
+];
diff --git a/src/lib/mocks/api.ts b/src/lib/mocks/api.ts
new file mode 100644
index 0000000..826e6bf
--- /dev/null
+++ b/src/lib/mocks/api.ts
@@ -0,0 +1,41 @@
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export type BookingPayload = {
+ roomId: string;
+ email: string;
+ flightBookingNumber: string;
+ arrivalTime: string;
+};
+
+export type PaymentPayload = {
+ totalCents: number;
+ last4?: string;
+};
+
+export async function submitBookingHold(
+ payload: BookingPayload,
+): Promise<{ reference: string }> {
+ void payload;
+ await delay(900 + Math.random() * 400);
+ return {
+ reference: `SHY-${Date.now().toString(36).toUpperCase()}`,
+ };
+}
+
+export async function processPayment(
+ payload: PaymentPayload,
+): Promise<{ confirmationId: string; paidAt: string }> {
+ void payload;
+ await delay(1100 + Math.random() * 500);
+ const id =
+ typeof crypto !== "undefined" && crypto.randomUUID
+ ? crypto.randomUUID().slice(0, 8)
+ : Math.random().toString(36).slice(2, 10);
+ const confirmationId = `PAY-${id.toUpperCase()}`;
+ return {
+ confirmationId,
+ paidAt: new Date().toISOString(),
+ };
+}
diff --git a/src/lib/mocks/bookingReviews.ts b/src/lib/mocks/bookingReviews.ts
new file mode 100644
index 0000000..693d270
--- /dev/null
+++ b/src/lib/mocks/bookingReviews.ts
@@ -0,0 +1,62 @@
+/**
+ * Illustrative guest reviews shown in the nav — style inspired by Booking.com.
+ * Replace copy/URLs with live data from your Booking.com property page when available.
+ */
+export type BookingStyleReview = {
+ id: string;
+ author: string;
+ country: string;
+ rating: number;
+ maxRating: number;
+ title: string;
+ text: string;
+ stayDate: string;
+ roomType: string;
+};
+
+export const bookingStyleReviews: BookingStyleReview[] = [
+ {
+ id: "1",
+ author: "Sarah M.",
+ country: "United Kingdom",
+ rating: 9.2,
+ maxRating: 10,
+ title: "Exceptional stay in Addis",
+ text: "Spotless suites, attentive team, and a perfect base for meetings. Breakfast at FeastVille was a highlight — we’ll return.",
+ stayDate: "October 2025",
+ roomType: "Connecting Suite",
+ },
+ {
+ id: "2",
+ author: "Daniel K.",
+ country: "Germany",
+ rating: 8.8,
+ maxRating: 10,
+ title: "Great location & comfort",
+ text: "Quiet rooms, strong Wi‑Fi, and easy access to the city. The junior studio had everything we needed for a week of work.",
+ stayDate: "September 2025",
+ roomType: "Junior Studio",
+ },
+ {
+ id: "3",
+ author: "Hanna T.",
+ country: "Ethiopia",
+ rating: 9.6,
+ maxRating: 10,
+ title: "Family trip made easy",
+ text: "We booked the penthouse for a celebration — space, views, and service exceeded expectations. Kids loved the IPTV selection.",
+ stayDate: "August 2025",
+ roomType: "4 Bedroom Penthouse",
+ },
+];
+
+export function averageBookingStyleRating(
+ list: BookingStyleReview[] = bookingStyleReviews,
+): number {
+ if (!list.length) return 0;
+ const sum = list.reduce((a, r) => a + r.rating, 0);
+ return Math.round((sum / list.length) * 10) / 10;
+}
+
+/** Aggregate score shown in the reviews dialog (out of 5), with circle “star” row */
+export const overallRatingOutOfFive = 4.5;
diff --git a/src/lib/mocks/meetingSpaces.ts b/src/lib/mocks/meetingSpaces.ts
new file mode 100644
index 0000000..b245f0a
--- /dev/null
+++ b/src/lib/mocks/meetingSpaces.ts
@@ -0,0 +1,86 @@
+import type { AmenityWithIcon } from "./amenities";
+
+export type MeetingSpace = {
+ slug: string;
+ name: string;
+ shortDescription: string;
+ longDescription: string;
+ capacity: string;
+ floor: string;
+ image: string;
+ gallery: string[];
+ amenities: AmenityWithIcon[];
+ layouts: string[];
+ catering: string[];
+ /** Mock half-day rate in USD for display (converted via currency switcher) */
+ halfDayRateUsd: number;
+};
+
+export const meetingSpaces: MeetingSpace[] = [
+ {
+ slug: "serenity",
+ name: "Serenity Meeting Room",
+ shortDescription: "Versatile event space for up to 100 guests on the 1st floor.",
+ longDescription:
+ "Serenity is designed for board sessions, cocktail receptions, and medium-scale corporate events. Natural light options, flexible seating, and dedicated support for AV and catering make it the hotel’s flagship meeting venue.",
+ capacity: "Up to 100 guests (theatre / cocktail configurations)",
+ floor: "1st floor",
+ image:
+ "https://images.unsplash.com/photo-1497366216548-37526070297c?w=1200&q=80",
+ gallery: [
+ "https://images.unsplash.com/photo-1497366216548-37526070297c?w=1200&q=80",
+ "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1200&q=80",
+ "https://images.unsplash.com/photo-1511578314322-379afb476865?w=1200&q=80",
+ ],
+ amenities: [
+ { icon: "wifi", label: "High-speed Wi‑Fi / LAN" },
+ { icon: "projector", label: "Projector & screen" },
+ { icon: "microphone", label: "Wireless microphones" },
+ { icon: "clipboard", label: "Flip charts & stationery" },
+ { icon: "thermometer", label: "Climate control" },
+ { icon: "handshake", label: "Dedicated event coordinator (on request)" },
+ { icon: "doorOpen", label: "Breakout foyer access" },
+ { icon: "accessibility", label: "Accessible routes" },
+ ],
+ layouts: ["Boardroom", "U-shape", "Theatre", "Classroom", "Cocktail / standing"],
+ catering: ["Buffet menus", "Tea & coffee breaks", "Working lunch packages", "Gala dinner (via FeastVille)"],
+ halfDayRateUsd: 850,
+ },
+ {
+ slug: "fasika",
+ name: "Fasika Board Room",
+ shortDescription: "Executive board room for 25–30 guests — intimate and fully equipped.",
+ longDescription:
+ "Fasika offers privacy and polish for leadership offsites, signing ceremonies, and focused workshops. Sound-treated walls, ergonomic seating, and premium coffee service keep sessions productive.",
+ capacity: "25–30 guests (boardroom style)",
+ floor: "1st floor",
+ image:
+ "https://images.unsplash.com/photo-1560179707-f14e90ef3623?w=1200&q=80",
+ gallery: [
+ "https://images.unsplash.com/photo-1560179707-f14e90ef3623?w=1200&q=80",
+ "https://images.unsplash.com/photo-1542744173-8e7e53415bb0?w=1200&q=80",
+ "https://images.unsplash.com/photo-1600880292203-757bb62b4baf?w=1200&q=80",
+ ],
+ amenities: [
+ { icon: "monitor", label: "4K display & HDMI / USB-C" },
+ { icon: "video", label: "Video-conferencing ready" },
+ { icon: "chair", label: "Executive leather seating" },
+ { icon: "volumeMuted", label: "Sound dampening" },
+ { icon: "restroom", label: "Private washroom adjacency" },
+ { icon: "pen", label: "Notepads & pens" },
+ { icon: "droplet", label: "Complimentary mineral water" },
+ { icon: "phone", label: "Dedicated phone line (on request)" },
+ ],
+ layouts: ["Boardroom", "Interview (2–4 pax)", "Small workshop"],
+ catering: ["Executive breakfast", "Coffee & pastries", "Light lunch boxes"],
+ halfDayRateUsd: 420,
+ },
+];
+
+export function getMeetingSpaceBySlug(slug: string): MeetingSpace | undefined {
+ return meetingSpaces.find((m) => m.slug === slug);
+}
+
+export function getAllMeetingSlugs(): string[] {
+ return meetingSpaces.map((m) => m.slug);
+}
diff --git a/src/lib/mocks/outlets.ts b/src/lib/mocks/outlets.ts
new file mode 100644
index 0000000..3ad4914
--- /dev/null
+++ b/src/lib/mocks/outlets.ts
@@ -0,0 +1,78 @@
+export type Outlet = {
+ slug: string;
+ name: string;
+ tagline: string;
+ bullets: string[];
+ image: string;
+ floor?: string;
+ /** Link to detail page when set (e.g. meeting rooms) */
+ detailHref?: string;
+};
+
+export const outlets: Outlet[] = [
+ {
+ slug: "feastville",
+ name: "FeastVille Restaurant",
+ tagline: "Full American breakfast to theme nights — savour every moment.",
+ bullets: [
+ "Full American breakfast",
+ "Traditional & international menu",
+ "Theme nights selection",
+ "Room service menu",
+ ],
+ image:
+ "https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=1200&q=80",
+ },
+ {
+ slug: "central-cafe",
+ name: "Central Cafe",
+ tagline: "Purely urban vibes — coffee at the heart of the city.",
+ bullets: [
+ "Your perfect rendezvous in the city centre",
+ "Ideal to initiate, elevate & conclude your day",
+ ],
+ image:
+ "https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=1200&q=80",
+ },
+ {
+ slug: "tabsia",
+ name: "TABSIA Bar",
+ tagline: "Cocktails, spirits, and a refined atmosphere.",
+ bullets: [
+ "Located on the 1st floor",
+ "Cocktails, spirits & more",
+ "Unwind after a long day",
+ ],
+ floor: "1st floor",
+ image:
+ "https://images.unsplash.com/photo-1551024506-0bccd828d307?w=1200&q=80&auto=format&fit=crop",
+ },
+ {
+ slug: "serenity",
+ name: "Serenity Meeting Room",
+ tagline: "Board meetings, cocktails, and events up to 100 guests.",
+ bullets: [
+ "Up to 100 pax",
+ "Fully equipped with basics & stationeries",
+ "Buffet or tea break menus",
+ ],
+ floor: "1st floor",
+ detailHref: "/meetings/serenity",
+ image:
+ "https://images.unsplash.com/photo-1497366216548-37526070297c?w=1200&q=80",
+ },
+ {
+ slug: "fasika",
+ name: "Fasika Board Room",
+ tagline: "Intimate executive sessions for 25–30 guests.",
+ bullets: [
+ "25–30 pax",
+ "Board & cocktail setups",
+ "Equipment & catering options",
+ ],
+ floor: "1st floor",
+ detailHref: "/meetings/fasika",
+ image:
+ "https://images.unsplash.com/photo-1560179707-f14e90ef3623?w=1200&q=80",
+ },
+];
diff --git a/src/lib/mocks/rooms.ts b/src/lib/mocks/rooms.ts
new file mode 100644
index 0000000..c1a27e7
--- /dev/null
+++ b/src/lib/mocks/rooms.ts
@@ -0,0 +1,103 @@
+export type Room = {
+ id: string;
+ slug: string;
+ name: string;
+ shortDescription: string;
+ longDescription: string;
+ nightlyRate: number;
+ maxGuests: number;
+ beds: string;
+ sizeSqM: number;
+ view: string;
+ highlights: string[];
+ gallery: string[];
+ tourEmbedUrl: string | null;
+};
+
+export const rooms: Room[] = [
+ {
+ id: "penthouse",
+ slug: "four-bedroom-penthouse",
+ name: "The 4 Bedroom Penthouse",
+ shortDescription: "Our flagship residence with panoramic views and full kitchenette.",
+ longDescription:
+ "Experience elevated living in our four-bedroom penthouse — expansive layouts, state-of-the-art kitchenette, and amazing views over Addis Ababa. Ideal for extended stays and distinguished guests who expect space, privacy, and premium finishes.",
+ nightlyRate: 485,
+ maxGuests: 8,
+ beds: "4 bedrooms — mix of king and twin configurations",
+ sizeSqM: 220,
+ view: "City skyline",
+ highlights: ["Private routers", "IPTV", "Mini bar", "In-room safe"],
+ gallery: [
+ "https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=1200&q=80&auto=format&fit=crop",
+ "https://images.unsplash.com/photo-1631049307264-da0ec9d70304?w=1200&q=80&auto=format&fit=crop",
+ "https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=1200&q=80&auto=format&fit=crop",
+ ],
+ tourEmbedUrl: null,
+ },
+ {
+ id: "standard",
+ slug: "standard-rooms",
+ name: "Standard Rooms",
+ shortDescription: "Refined comfort with every essential amenity.",
+ longDescription:
+ "Our standard rooms combine restful design with practical luxury: premium bedding, dedicated workspace, IPTV, and seamless Wi‑Fi / LAN. Perfect for business and leisure travellers who value consistency and calm.",
+ nightlyRate: 120,
+ maxGuests: 2,
+ beds: "1 King or 2 Twin",
+ sizeSqM: 28,
+ view: "City or courtyard",
+ highlights: ["B/B fast", "Iron & board", "Laundry (paid)", "Safe box"],
+ gallery: [
+ "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1200&q=80",
+ "https://images.unsplash.com/photo-1566665797739-1674de7a215a?w=1200&q=80",
+ ],
+ tourEmbedUrl: null,
+ },
+ {
+ id: "connecting-suite",
+ slug: "connecting-suite",
+ name: "Connecting Suite",
+ shortDescription: "Flexible suites — convert to a spacious family layout.",
+ longDescription:
+ "Connecting suite rooms with the option of converting to family suites. Enjoy separate living and sleeping zones, kitchenette access where applicable, and the same premium amenities found across the property.",
+ nightlyRate: 210,
+ maxGuests: 5,
+ beds: "1 King + connecting twin room",
+ sizeSqM: 55,
+ view: "City",
+ highlights: ["Family-friendly layout", "Kitchenette", "IPTV", "Shuttle"],
+ gallery: [
+ "https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=1200&q=80",
+ "https://images.unsplash.com/photo-1591088398332-8a7791972843?w=1200&q=80",
+ ],
+ tourEmbedUrl: null,
+ },
+ {
+ id: "junior-studio",
+ slug: "junior-studios",
+ name: "Junior Studios",
+ shortDescription: "Compact sophistication for solo travellers and short stays.",
+ longDescription:
+ "Junior studios offer a smart open plan with kitchenette, premium Wi‑Fi, IPTV, and efficient storage — designed for guests who want independence without sacrificing hotel service.",
+ nightlyRate: 95,
+ maxGuests: 2,
+ beds: "1 Queen",
+ sizeSqM: 32,
+ view: "Urban",
+ highlights: ["Kitchenette", "Mini bar", "Private router option"],
+ gallery: [
+ "https://images.unsplash.com/photo-1522771739844-6a9f6d5f14af?w=1200&q=80",
+ "https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=1200&q=80",
+ ],
+ tourEmbedUrl: null,
+ },
+];
+
+export function getRoomBySlug(slug: string): Room | undefined {
+ return rooms.find((r) => r.slug === slug);
+}
+
+export function getAllRoomSlugs(): string[] {
+ return rooms.map((r) => r.slug);
+}
diff --git a/src/lib/mocks/site.ts b/src/lib/mocks/site.ts
new file mode 100644
index 0000000..de907e7
--- /dev/null
+++ b/src/lib/mocks/site.ts
@@ -0,0 +1,34 @@
+/** Site-wide mock config — replace embed URLs when real Matterport/360 tours exist. */
+export const siteConfig = {
+ name: "Shitaye Suite Hotel",
+ tagline: "The Unwinding Choice",
+ city: "Addis Ababa",
+ address:
+ "Prime location — geo-convenient, close to key attractions and major businesses.",
+ phones: ["+251 96 688 4400", "+251 96 688 2200", "+251 11 46 21000"],
+ /** Primary number shown on FAB / quick call */
+ primaryPhone: "+251 96 688 4400",
+ email: "reservation@shitayesuitehotel.com",
+ /** Departments (from official site) */
+ departments: [
+ { label: "Marketing", phones: ["+251 96 688 4400", "+251 96 688 2200"] },
+ { label: "Reception", phones: ["+251 11 46 21000"] },
+ ],
+ videoTourUrl: "https://www.youtube.com/watch?v=oH4hH1P7vdM",
+ hotelTourEmbedUrl: null as string | null,
+ /** Property listing (guest reviews, photos) */
+ bookingComReviewsUrl: "https://www.booking.com/hotel/et/shitaye-suite.html",
+ /**
+ * Lobby / lounge photo from the Booking.com gallery (same listing as above).
+ * Caption on Booking: living area with seating — property-authentic asset.
+ */
+ lobbyImageUrl:
+ "https://cf.bstatic.com/xdata/images/hotel/max1024x768/536142684.jpg?k=e550cdbc87e2b08b7fd6b261d0c719149024f47369a5f53a628fca9630631bb6&o=",
+ social: {
+ facebook: "https://www.facebook.com/shitayesuitehotel/",
+ twitter: "https://twitter.com/ShitayeSuite",
+ whatsapp: "https://wa.me/0966884400",
+ instagram: "https://instagram.com/shitaye_suite_hotel",
+ },
+ taxRate: 0.15,
+};
diff --git a/src/lib/mocks/wellness.ts b/src/lib/mocks/wellness.ts
new file mode 100644
index 0000000..0d31caa
--- /dev/null
+++ b/src/lib/mocks/wellness.ts
@@ -0,0 +1,48 @@
+import type { AmenityWithIcon } from "./amenities";
+
+export type WellnessFacility = {
+ id: string;
+ title: string;
+ subtitle: string;
+ description: string;
+ image: string;
+ amenities: AmenityWithIcon[];
+ hours: string;
+};
+
+export const wellnessFacilities: WellnessFacility[] = [
+ {
+ id: "gym",
+ title: "Fitness centre",
+ subtitle: "Train on your schedule",
+ description:
+ "Cardio machines, free weights, and functional training space — maintained daily and stocked with fresh towels and chilled water. Perfect before meetings or after long flights.",
+ image:
+ "https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=1200&q=80",
+ amenities: [
+ { icon: "treadmill", label: "Treadmills & ellipticals" },
+ { icon: "dumbbell", label: "Dumbbells & kettlebells" },
+ { icon: "stretch", label: "Stretching zone" },
+ { icon: "towel", label: "Towel service" },
+ { icon: "headphones", label: "Bluetooth audio (personal headsets)" },
+ ],
+ hours: "6:00 — 22:00 daily",
+ },
+ {
+ id: "spa",
+ title: "Spa & wellness",
+ subtitle: "Restore and unwind",
+ description:
+ "Therapeutic massages, express treatments, and calming lounges inspired by Ethiopian botanicals. Book ahead for couples’ rituals or post-event recovery sessions.",
+ image:
+ "https://images.unsplash.com/photo-1540555700478-4be289fbecef?w=1200&q=80",
+ amenities: [
+ { icon: "massage", label: "Signature massage menu" },
+ { icon: "steam", label: "Steam experience (select days)" },
+ { icon: "leaf", label: "Aromatherapy add-ons" },
+ { icon: "lounge", label: "Private treatment suites" },
+ { icon: "boutique", label: "Retail boutique" },
+ ],
+ hours: "10:00 — 20:00 · appointments recommended",
+ },
+];
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cf9c65d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}
|