feat: soften brand gold, hero copy; OpenNext Cloudflare config

- Replace flat mustard with softer champagne tones and gentler shadows
- Hero eyebrow: Book direct · Shitaye Suite Hotel
- Lighter pattern grid, chips, badges, card hover ring; meetings band ring toned down
- Ignore .dev.vars and .wrangler for local secrets and cache

Made-with: Cursor
This commit is contained in:
“kirukib” 2026-03-25 00:18:51 +03:00
parent 0b7c0fcd2b
commit 1a37b91795
29 changed files with 6315 additions and 190 deletions

7
.gitignore vendored
View File

@ -39,3 +39,10 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# OpenNext
.open-next
# Cloudflare / Wrangler local secrets (do not commit)
.dev.vars
.wrangler/

220
ADMIN_BOOKING_README.md Normal file
View File

@ -0,0 +1,220 @@
# Booking & payments — administrator panel specification
This document describes the **guest booking domain** as implemented in the **Shitaye FrontEnd** demo today, and what a **separate administrator application** should support to manage **bookings**, **holds**, and **payments**. Use it as a blueprint when you build the admin API, database schema, and UI.
The public site lives in this repository (`Shitaye-FrontEnd`). The admin panel is **not** included here; this README is the handoff for that work.
---
## 1. Goals of the admin system
- **List and search** reservations (by date, status, guest, reference, room, payment state).
- **Open a booking** and see full guest, stay, pricing, flight/arrival, coupons, and payment metadata.
- **Reconcile payments** (paid, pending, failed, refunded) against holds or confirmed bookings.
- **Operational actions** (policy-dependent): cancel hold, extend hold, mark no-show, issue refund, add internal notes, export for finance.
---
## 2. Current public app (this repo) — important limitations
Today the storefront is a **client-side mock**:
- State lives in **React context** (`BookingProvider`) in the browser — **nothing is persisted** to your backend when a guest “books”.
- `submitBookingHold` and `processPayment` in `src/lib/mocks/api.ts` only **simulate latency** and return fake IDs (`SHY-…`, `PAY-…`).
- **No real card processing**, no webhooks, no idempotent payment IDs.
**Implication for admin:** you will introduce a **real backend** (or extend an existing PMS/booking engine). The admin panel should talk to **that** system, not to the in-memory Next.js state.
**Reference files in this repo**
| Area | Path |
|------|------|
| Guest & stay state | `src/context/BookingContext.tsx` |
| Guest shape | `GuestDetails` in same file |
| Hold / payment mock API | `src/lib/mocks/api.ts` |
| Room catalogue (ids, rates) | `src/lib/mocks/rooms.ts` |
| Tax rate | `src/lib/mocks/site.ts` (`taxRate`, e.g. `0.15` = 15%) |
---
## 3. Domain model (what to persist)
### 3.1 Guest (contact + arrival)
Aligned with `GuestDetails`:
| Field | Type | Notes |
|-------|------|--------|
| `firstName` | string | |
| `lastName` | string | |
| `email` | string | Primary contact; unique per booking or account policy TBD |
| `phone` | string | E.164 recommended in production |
| `flightBookingNumber` | string | PNR / record locator / ticket ref |
| `arrivalTime` | string | Currently `HH:mm` from UI; store as time or datetime with timezone (Addis Ababa) in production |
### 3.2 Stay
| Field | Type | Notes |
|-------|------|--------|
| `checkIn` | date (ISO date) | `YYYY-MM-DD` in UI |
| `checkOut` | date (ISO date) | |
| `guests` | integer | 112 in UI |
| `roomId` | string | Catalogue key: `penthouse`, `standard`, `connecting-suite`, `junior-studio` (see `rooms.ts`) |
| `nights` | integer | Derived; validate against check-in/out |
### 3.3 Pricing (display currency vs ledger)
The storefront shows **USD catalogue rates** on the server-side math, with an optional **display currency** (EUR, GBP, AED, etc.) via `CurrencyContext` / `src/lib/currency.ts`.
**Admin / accounting recommendation**
- Store amounts in a **single base currency** (e.g. ETB or USD) per property policy.
- Store **FX snapshot** at booking or payment time if you show multi-currency to guests.
- Line items to persist (mirroring checkout):
| Concept | Source in app |
|---------|----------------|
| Nightly subtotal | `nightlyRate × nights` from room catalogue |
| Coupon code | string |
| Discount % | e.g. `10` or `5` (mock codes `SHITAYE10`, `WELCOME5`) |
| Discount amount | % of subtotal |
| Tax | `siteConfig.taxRate` × taxable base (after discount) |
| **Total** | Grand total charged or to be charged |
### 3.4 Hold vs payment (lifecycle)
The UI distinguishes:
| Concept | Flag / field | Meaning |
|---------|----------------|--------|
| Hold reference | `holdReference` | e.g. `SHY-…` — created when guest continues from `/booking` (pay now or pay later path) |
| Pay later | `payLaterHold` | `true` if guest chose “Reserve now — pay later” before visiting payment |
| Payment confirmation | `confirmationId` | e.g. `PAY-…` after successful (mock) payment |
| Paid timestamp | `paidAt` | ISO datetime string |
**Suggested persisted statuses** (you can rename):
1. `draft` — abandoned cart (optional)
2. `held` — hold created, **not** paid (`payLaterHold` may be true or false; both paths create a hold in the current flow)
3. `payment_pending` — guest on payment step (optional transient)
4. `confirmed` — payment succeeded (`confirmationId` + `paidAt`)
5. `cancelled` / `expired` — hold released or timeout
Map the mocks `payLaterHold` to your policy: e.g. **held unpaid** vs **held with intent to pay later**.
---
## 4. Mock API payloads (prototypes for real endpoints)
Todays TypeScript types in `src/lib/mocks/api.ts`:
### 4.1 Create hold (after guest details + flight)
`BookingPayload`:
- `roomId`, `email`, `flightBookingNumber`, `arrivalTime`
**Real API should also accept:** `checkIn`, `checkOut`, `guests`, full guest name/phone, pricing breakdown, coupon fields — everything needed to reconstruct the reservation without trusting the client for totals.
**Response (mock):** `{ reference: string }`
### 4.2 Process payment
`PaymentPayload`:
- `totalCents` (integer; mock uses USD × 100)
- `last4` (optional; card last four — never store full PAN in admin DB)
**Response (mock):** `{ confirmationId: string, paidAt: string }`
**Production:** integrate PSP (Stripe, Chapa, etc.), store **payment intent ID**, **status**, **receipt URL**, and **audit trail**; admin reads from your payment service or synced tables.
---
## 5. Room catalogue keys (admin filters & reports)
Use the same `id` values as `src/lib/mocks/rooms.ts` unless you migrate to UUIDs:
| `roomId` | Display name (short) |
|----------|----------------------|
| `penthouse` | The 4 Bedroom Penthouse |
| `standard` | Standard Rooms |
| `connecting-suite` | Connecting Suite |
| `junior-studio` | Junior Studios |
Each room has `nightlyRate` (USD in mock), `maxGuests`, `slug` for public URLs.
---
## 6. Administrator panel — recommended features
### 6.1 Dashboard
- Todays arrivals / departures
- Unpaid holds (pay-later + overdue)
- Revenue snapshot (range selector)
### 6.2 Bookings list
- Filters: status, date range (stay or created), room, email, hold ref, payment ref
- Sort: check-in, created at, total
- Bulk export CSV (finance)
### 6.3 Booking detail
- Guest, stay, room, pricing lines, coupon, flight/PNR, arrival time
- Payment section: provider, amount, currency, status, timestamps, refund button (if allowed)
- Internal notes (staff-only), activity log
### 6.4 Payments
- List transactions linked to bookings
- Reconciliation view (settled vs pending)
- Manual “mark paid” only if you accept bank transfer / cash — with strict RBAC
### 6.5 Configuration (optional)
- Tax rate, active coupon codes, hold TTL, room rates (or sync from PMS)
---
## 7. Security & compliance
- **RBAC:** roles such as `viewer`, `front_desk`, `finance`, `superadmin`.
- **PII:** encrypt at rest where required; minimize data in logs; GDPR-style export/erase if you serve EU/UK guests.
- **PCI:** never store raw card data; use tokenization via your PSP; admin UI shows only last4 / brand if provided.
- **Audit:** who changed status, refunds, or manual overrides.
---
## 8. Integrating the public Next.js app later
Replace `src/lib/mocks/api.ts` calls with `fetch`/`axios` to your backend:
1. **POST** `/bookings/hold` — persist reservation + return real `holdReference`
2. **POST** `/bookings/:id/pay` or PSP redirect + **webhook** — update status to confirmed
3. Optional **GET** `/bookings/:ref` for guest “lookup my booking”
The admin app should use the **same API** (or an internal admin API with stronger scopes) so there is a single source of truth.
---
## 9. Open decisions (product / engineering)
- Single vs multi-property admin
- Whether “pay later” holds **expire** automatically and how guests resume payment (magic link vs login)
- Official currency of record and whether ETB is required for local compliance
- Connection to an existing PMS (Opera, Cloudbeds, etc.) vs custom DB only
---
## 10. Document maintenance
When you change the guest booking flow in **Shitaye-FrontEnd**, update:
- Section **3** (fields), **4** (payloads), and **5** (room ids)
- This file path: `ADMIN_BOOKING_README.md` (repository root)
---
*Prepared for Yaltopia / Shitaye Suite Hotel — booking management administrator development.*

View File

@ -11,3 +11,5 @@ const nextConfig: NextConfig = {
};
export default nextConfig;
import('@opennextjs/cloudflare').then(m => m.initOpenNextCloudflareForDev());

9
open-next.config.ts Normal file
View File

@ -0,0 +1,9 @@
// default open-next.config.ts file created by @opennextjs/cloudflare
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
// import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
export default defineCloudflareConfig({
// For best results consider enabling R2 caching
// See https://opennext.js.org/cloudflare/caching for more details
// incrementalCache: r2IncrementalCache
});

5624
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,15 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"dev": "next dev",
"start": "next start",
"lint": "eslint ."
"lint": "eslint .",
"patch-open-next-handler": "node scripts/patch-open-next-handler.js",
"preview": "opennextjs-cloudflare build && npm run patch-open-next-handler && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && npm run patch-open-next-handler && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && npm run patch-open-next-handler && opennextjs-cloudflare upload",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
},
"dependencies": {
"next": "16.2.1",
@ -14,6 +19,8 @@
"react-dom": "19.2.4"
},
"devDependencies": {
"@opennextjs/aws": "^3.9.16",
"@opennextjs/cloudflare": "^1.17.1",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
@ -21,6 +28,7 @@
"eslint": "^9",
"eslint-config-next": "16.2.1",
"tailwindcss": "^4",
"typescript": "^5"
"typescript": "^5",
"wrangler": "^4.76.0"
}
}

4
public/_headers Normal file
View File

@ -0,0 +1,4 @@
# https://developers.cloudflare.com/workers/static-assets/headers
# https://opennext.js.org/cloudflare/caching#static-assets-caching
/_next/static/*
Cache-Control: public,max-age=31536000,immutable

View File

@ -0,0 +1,41 @@
/* eslint-disable no-console */
const fs = require("node:fs");
const path = require("node:path");
function patchIfNeeded(targetPath) {
if (!fs.existsSync(targetPath)) {
console.warn(`[opennext patch] Skip: missing file: ${targetPath}`);
return false;
}
const text = fs.readFileSync(targetPath, "utf8");
const oldStr = "throw new Error(`Unexpected loadManifest(${path2}) call!`)";
const newStr = "return{}";
const count = text.split(oldStr).length - 1;
if (count === 0) {
console.log("[opennext patch] handler already patched (or no-op).");
return false;
}
if (count !== 1) {
console.warn(`[opennext patch] Unexpected match count: ${count}`);
}
const patched = text.replace(oldStr, newStr);
fs.writeFileSync(targetPath, patched, "utf8");
console.log(`[opennext patch] Patched handler loadManifest in ${path.relative(process.cwd(), targetPath)}`);
return true;
}
const target = path.join(
process.cwd(),
".open-next",
"server-functions",
"default",
"handler.mjs",
);
patchIfNeeded(target);

View File

@ -69,7 +69,7 @@ export function BookingPageClient() {
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
Book your stay
</p>
<h1 className="mt-2 font-display text-3xl md:text-4xl">
<h1 className="mt-2 font-heading text-3xl md:text-4xl">
It only takes a moment
</h1>
<p className="mt-2 text-sm text-[var(--color-muted)]">
@ -192,7 +192,7 @@ export function BookingPageClient() {
disabled={!canContinue || pending !== null}
aria-busy={pending === "payment"}
onClick={() => placeHold("payment")}
className="w-full rounded-full bg-[var(--color-text)] py-4 text-sm font-semibold text-white transition hover:bg-[var(--color-primary)] disabled:cursor-not-allowed disabled:opacity-50"
className="btn-mustard w-full py-4 text-sm disabled:cursor-not-allowed"
>
{pending === "payment" ? "Please wait…" : "Continue to payment"}
</button>
@ -201,7 +201,7 @@ export function BookingPageClient() {
disabled={!canContinue || pending !== null}
aria-busy={pending === "reserve"}
onClick={() => placeHold("reserve")}
className="w-full rounded-full border-2 border-[var(--color-primary)] bg-transparent py-3.5 text-sm font-semibold text-[var(--color-primary)] transition hover:bg-[var(--color-primary)] hover:text-[var(--color-on-primary)] disabled:cursor-not-allowed disabled:opacity-50"
className="w-full rounded-full border-2 border-[var(--color-primary)] bg-transparent py-3.5 text-sm font-semibold text-[var(--color-primary)] transition hover:border-transparent hover:bg-gradient-to-br hover:from-[var(--color-accent-highlight)] hover:via-[var(--color-accent)] hover:to-[var(--color-accent-deep)] hover:text-[var(--color-on-accent)] hover:shadow-[var(--shadow-mustard)] disabled:cursor-not-allowed disabled:opacity-50"
>
{pending === "reserve" ? "Saving your hold…" : "Reserve now — pay later"}
</button>

View File

@ -39,7 +39,7 @@ export default function ConfirmationPage() {
>
</div>
<h1 className="mt-8 font-display text-3xl md:text-4xl">Your booking is confirmed</h1>
<h1 className="mt-8 font-heading text-3xl md:text-4xl">Your booking is confirmed</h1>
<p className="mt-3 text-sm text-[var(--color-muted)]">
Thank you, {guest.firstName}. A mock itinerary email would be sent to {guest.email}.
</p>
@ -92,7 +92,7 @@ export default function ConfirmationPage() {
<Link
href="/"
onClick={() => resetBooking()}
className="mt-10 inline-flex rounded-full bg-[var(--color-text)] px-10 py-3.5 text-sm font-semibold text-white hover:bg-[var(--color-primary)]"
className="btn-mustard mt-10 inline-flex px-10 py-3.5 text-sm"
>
Back to home
</Link>

View File

@ -1,20 +1,46 @@
@import "tailwindcss";
/*
Shitaye Suite Hotel dark teal · mustard gold · warm gray.
Teal: chrome, links, wordmark accents. Mustard: primary buttons & CTAs. Gray: borders / structure.
*/
:root {
--color-bg: #faf7f2;
/* —— Brand base —— */
--color-brand-teal: #174746;
--color-brand-teal-hover: #0f3234;
/* Softer champagne gold — muted, smooth; still readable with teal type */
--color-brand-mustard: #c9b07e;
--color-brand-mustard-hover: #bda270;
--color-accent-highlight: #e4dcc8;
--color-accent-deep: #a89462;
--color-brand-gray: #bdbbb4;
/* —— Semantic —— */
--color-bg: #f5f4f1;
--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;
--color-surface-muted: #e8e6e2;
--color-iced-mint: #e6ebe9;
--color-navy: var(--color-brand-teal);
--color-text: #1c1c1a;
--color-muted: #5c5a54;
--color-border: #a9a79f;
--color-primary: var(--color-brand-teal);
--color-primary-hover: var(--color-brand-teal-hover);
--color-on-primary: #f4f5f4;
--color-accent: var(--color-brand-mustard);
--color-accent-hover: var(--color-brand-mustard-hover);
--color-accent-soft: #faf7ef;
--color-on-accent: #174746;
--shadow-mustard: 0 3px 14px rgba(120, 98, 62, 0.2);
--shadow-mustard-hover: 0 6px 22px rgba(120, 98, 62, 0.28);
--color-lemon-green: #9cae6b;
--color-success: #174746;
/* Rollgates: hotel name in navbar only */
--font-nav: "Rollgates Victoria", "Cormorant Garamond", Georgia, serif;
/* Avenir everywhere else (Mulish as web fallback) */
--font-heading: "Avenir Next", "Avenir", "Mulish", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-ui: "Avenir Next", "Avenir", "Mulish", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-ethiopic: "Noto Sans Ethiopic", "Noto Sans", sans-serif;
}
@theme inline {
@ -33,8 +59,108 @@ body {
font-family: var(--font-ui);
}
.font-display {
font-family: var(--font-display);
/* Headings site-wide (not navbar) */
.font-heading {
font-family: var(--font-heading);
}
/* Hotel name + main nav chrome only */
.font-nav {
font-family: var(--font-nav);
}
/* Amharic copy — guideline: Addis Abeba Unicode; web substitute: Noto Sans Ethiopic */
.font-ethiopic {
font-family: var(--font-ethiopic);
}
/* Branded grid — soft gold hint + faint teal */
.bg-pattern-brand-gold {
background-color: var(--color-bg);
background-image:
linear-gradient(90deg, rgba(200, 175, 130, 0.09) 1px, transparent 1px),
linear-gradient(rgba(23, 71, 70, 0.045) 1px, transparent 1px);
background-size: 28px 28px;
}
/* Primary CTAs — soft champagne gradient, gentle depth */
.btn-mustard {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
border-radius: 9999px;
font-weight: 600;
color: var(--color-on-accent);
background: linear-gradient(
165deg,
var(--color-accent-highlight) 0%,
var(--color-accent) 48%,
var(--color-accent-deep) 100%
);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.5),
inset 0 -1px 0 rgba(0, 0, 0, 0.06),
var(--shadow-mustard);
transition:
filter 0.2s ease,
box-shadow 0.2s ease,
transform 0.15s ease;
}
.btn-mustard:hover {
filter: brightness(1.03) saturate(1.03);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.55),
inset 0 -1px 0 rgba(0, 0, 0, 0.05),
var(--shadow-mustard-hover);
}
.btn-mustard:active {
transform: translateY(1px);
filter: brightness(0.99);
}
.btn-mustard:disabled {
opacity: 0.55;
cursor: not-allowed;
filter: grayscale(0.12) brightness(0.96);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* Compact rating / score badges (non-pill) */
.badge-mustard {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.375rem;
font-weight: 700;
font-size: 0.75rem;
line-height: 1rem;
padding: 0.125rem 0.5rem;
color: var(--color-on-accent);
background: linear-gradient(
180deg,
#e8ddd0 0%,
var(--color-accent) 55%,
var(--color-accent-deep) 100%
);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.45),
0 1px 4px rgba(120, 98, 62, 0.2);
}
/* Pills / chips — warm wash + soft gold ring */
.chip-mustard {
border-radius: 9999px;
border: 1px solid rgba(180, 155, 110, 0.35);
background: linear-gradient(
135deg,
rgba(252, 249, 242, 0.98) 0%,
rgba(255, 255, 255, 0.92) 100%
);
box-shadow: 0 1px 2px rgba(100, 80, 50, 0.06);
color: var(--color-primary);
font-weight: 600;
font-size: 0.75rem;
line-height: 1rem;
}
.grain::before {
@ -54,7 +180,9 @@ body {
}
.card-lift:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(28, 25, 23, 0.08);
box-shadow:
0 20px 44px rgba(23, 71, 70, 0.12),
0 0 0 1px rgba(200, 180, 140, 0.14);
}
@keyframes mock3d-rotate {

View File

@ -1,23 +1,8 @@
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",
@ -35,9 +20,22 @@ export default function RootLayout({
return (
<html
lang="en"
className={`${cormorant.variable} ${dmSans.variable} h-full antialiased`}
className="h-full antialiased"
>
<body className="min-h-full">
{/* Load Google fonts with standard links (avoids Next font internals on Cloudflare/OpenNext). */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link rel="preconnect" href="https://fonts.cdnfonts.com" />
{/* Rollgates: hotel wordmark only. Mulish: Avenir substitute for UI + headings. */}
<link
href="https://fonts.cdnfonts.com/css/rollgates-victoria"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Mulish:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Noto+Sans+Ethiopic:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<Providers>
<Shell>{children}</Shell>
</Providers>

View File

@ -3,10 +3,10 @@ import Link from "next/link";
export default function MeetingNotFound() {
return (
<div className="mx-auto max-w-lg px-4 py-24 text-center">
<h1 className="font-display text-3xl">Meeting space not found</h1>
<h1 className="font-heading text-3xl">Meeting space not found</h1>
<Link
href="/#meetings"
className="mt-8 inline-flex rounded-full bg-[var(--color-primary)] px-8 py-3 text-sm font-semibold text-white"
className="btn-mustard mt-8 inline-flex px-8 py-3 text-sm"
>
View venues
</Link>

View File

@ -51,7 +51,7 @@ export default async function MeetingSpacePage({ params }: Props) {
>
Dining & venues
</Link>
<h1 className="mt-2 font-display text-4xl text-white md:text-5xl">{space.name}</h1>
<h1 className="mt-2 font-heading text-4xl text-white md:text-5xl">{space.name}</h1>
<p className="mt-2 max-w-2xl text-sm text-white/90">{space.shortDescription}</p>
</div>
</div>
@ -59,7 +59,7 @@ export default async function MeetingSpacePage({ params }: Props) {
<div className="mx-auto max-w-7xl px-4 py-12 md:px-8 md:py-16">
<div className="grid gap-12 lg:grid-cols-[1fr_360px]">
<div>
<h2 className="font-display text-2xl">Overview</h2>
<h2 className="font-heading text-2xl">Overview</h2>
<p className="mt-4 leading-relaxed text-[var(--color-muted)]">{space.longDescription}</p>
<div className="mt-10 grid gap-4 sm:grid-cols-2">
@ -77,7 +77,7 @@ export default async function MeetingSpacePage({ params }: Props) {
</div>
<section className="mt-14">
<h2 className="font-display text-2xl">Amenities & equipment</h2>
<h2 className="font-heading text-2xl">Amenities & equipment</h2>
<ul className="mt-4 grid gap-2 sm:grid-cols-2">
{space.amenities.map((a) => (
<AmenityItem key={a.label} item={a} variant="card" />
@ -86,7 +86,7 @@ export default async function MeetingSpacePage({ params }: Props) {
</section>
<section className="mt-12">
<h2 className="font-display text-2xl">Layouts</h2>
<h2 className="font-heading text-2xl">Layouts</h2>
<ul className="mt-3 flex flex-wrap gap-2">
{space.layouts.map((l) => (
<li
@ -100,7 +100,7 @@ export default async function MeetingSpacePage({ params }: Props) {
</section>
<section className="mt-12">
<h2 className="font-display text-2xl">Catering</h2>
<h2 className="font-heading text-2xl">Catering</h2>
<ul className="mt-3 space-y-2 text-sm text-[var(--color-muted)]">
{space.catering.map((c) => (
<li key={c}>· {c}</li>
@ -122,7 +122,7 @@ export default async function MeetingSpacePage({ params }: Props) {
<MeetingHalfDayRate usdAmount={space.halfDayRateUsd} />
<a
href={`mailto:${siteConfig.email}?subject=${encodeURIComponent(`Event inquiry — ${space.name}`)}`}
className="mt-6 block w-full rounded-full bg-[var(--color-text)] py-3 text-center text-sm font-semibold text-white transition hover:bg-[var(--color-primary)]"
className="btn-mustard mt-6 block w-full py-3 text-center text-sm"
>
Request a proposal
</a>

View File

@ -6,6 +6,7 @@ import { OutletCard } from "@/components/OutletCard";
import { RoomCard } from "@/components/RoomCard";
import { VirtualTourBlock } from "@/components/VirtualTourBlock";
import { roomAmenities } from "@/lib/mocks/amenities";
import { bookingStyleReviews } from "@/lib/mocks/bookingReviews";
import { outlets } from "@/lib/mocks/outlets";
import { rooms } from "@/lib/mocks/rooms";
import { siteConfig } from "@/lib/mocks/site";
@ -29,38 +30,94 @@ export default function HomePage() {
<div className="absolute inset-0 bg-gradient-to-r from-black/75 via-black/45 to-black/25" />
<div className="relative mx-auto flex min-h-[78vh] max-w-7xl flex-col justify-end px-4 pb-10 pt-32 md:px-8 md:pb-14">
<p className="text-xs font-semibold uppercase tracking-[0.25em] text-white/80">
Official website
Book direct · Shitaye Suite Hotel
</p>
<h1 className="mt-4 max-w-3xl font-display text-4xl font-semibold leading-tight text-white md:text-6xl">
{siteConfig.tagline}
<h1 className="mt-4 max-w-4xl font-heading text-4xl font-semibold leading-tight text-white md:text-6xl">
Spacious Luxury Suites Near Bole Airport - Designed for Business & Long Stay
</h1>
<p className="mt-4 max-w-xl text-lg text-white/90">
Discover refined stays in Addis Ababa exceptional rooms, celebrated dining, and
spaces designed for connection.
<p className="mt-4 max-w-2xl text-lg text-white/90">
Direct booking benefits, flexible options, and trusted guest-rated hospitality in Addis
Ababa.
</p>
<div className="mt-5 flex flex-wrap gap-2 text-xs font-semibold text-white/95">
{["Near Bole Airport", "Best-rate direct booking", "Free airport shuttle"].map((item) => (
<span
key={item}
className="rounded-full border border-white/35 bg-black/25 px-3 py-1 backdrop-blur-sm"
>
{item}
</span>
))}
</div>
<div className="mt-10 w-full max-w-4xl">
<BookingSearchWidget />
</div>
</div>
</section>
<section className="bg-pattern-brand-gold">
<div className="mx-auto max-w-7xl px-4 py-12 md:px-8">
<div className="rounded-3xl border border-[var(--color-border)] bg-[var(--color-surface)] p-6 shadow-sm md:p-8">
<div className="flex flex-col gap-5 md:flex-row md:items-end md:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--color-primary)]">
Guest trust
</p>
<h2 className="mt-2 font-heading text-3xl md:text-4xl">
What guests say before they book
</h2>
<p className="mt-2 max-w-2xl text-sm text-[var(--color-muted)]">
Real feedback from recent stays helps new visitors book directly with confidence.
</p>
</div>
<Link
href={siteConfig.bookingComReviewsUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-semibold text-[var(--color-accent-deep)] underline-offset-4 transition hover:text-[var(--color-accent)] hover:underline"
>
Read all reviews
</Link>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-3">
{bookingStyleReviews.slice(0, 3).map((review) => (
<article
key={review.id}
className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-muted)] p-4"
>
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-semibold text-[var(--color-text)]">{review.author}</p>
<span className="badge-mustard">
{review.rating.toFixed(1)}/{review.maxRating}
</span>
</div>
<p className="mt-2 line-clamp-3 text-sm leading-relaxed text-[var(--color-muted)]">
"{review.text}"
</p>
<p className="mt-3 text-xs text-[var(--color-muted)]">
{review.country} · {review.stayDate}
</p>
</article>
))}
</div>
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-20 md:px-8">
<div className="grid gap-12 lg:grid-cols-2 lg:items-center">
<div>
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
About us
</p>
<h2 className="mt-3 font-display text-3xl text-[var(--color-text)] md:text-4xl">
<h2 className="mt-3 font-heading text-3xl text-[var(--color-text)] md:text-4xl">
Geo-convenient. Unmistakably Shitaye.
</h2>
<p className="mt-4 leading-relaxed text-[var(--color-muted)]">
Close to key places of attraction and major businesses or institutions your base
for work, culture, and rest. Begin your journey with us.
</p>
<Link
href="/#rooms"
className="mt-6 inline-flex rounded-full border-2 border-[var(--color-primary)] px-6 py-3 text-sm font-semibold text-[var(--color-primary)] transition hover:bg-[var(--color-primary)] hover:text-[var(--color-on-primary)]"
>
<Link href="/#rooms" className="btn-mustard mt-6 inline-flex px-6 py-3 text-sm">
View rooms
</Link>
</div>
@ -93,7 +150,7 @@ export default function HomePage() {
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
Stay with us
</p>
<h2 className="mt-2 font-display text-3xl md:text-4xl">Rooms & suites</h2>
<h2 className="mt-2 font-heading text-3xl md:text-4xl">Rooms & suites</h2>
<p className="mt-2 max-w-xl text-sm text-[var(--color-muted)]">
From junior studios to our four-bedroom penthouse every category includes premium
amenities and attentive service.
@ -101,7 +158,7 @@ export default function HomePage() {
</div>
<Link
href="/booking"
className="text-sm font-semibold text-[var(--color-primary)] hover:underline"
className="text-sm font-semibold text-[var(--color-accent-deep)] underline-offset-4 transition hover:text-[var(--color-accent)] hover:underline"
>
Book a room
</Link>
@ -123,7 +180,7 @@ export default function HomePage() {
<p className="text-xs font-semibold uppercase tracking-[0.25em] text-[var(--color-primary)]">
Wellness
</p>
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-[var(--color-text)] md:text-5xl">
<h2 className="mt-4 font-heading text-3xl font-semibold tracking-tight text-[var(--color-text)] md:text-5xl">
Gym & Spa
</h2>
<p className="mt-5 text-base leading-relaxed text-[var(--color-muted)] md:mt-6 md:text-lg md:leading-relaxed">
@ -151,13 +208,13 @@ export default function HomePage() {
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--color-primary)]">
{w.subtitle}
</p>
<h3 className="mt-3 font-display text-3xl text-[var(--color-text)] md:mt-4 md:text-4xl">
<h3 className="mt-3 font-heading text-3xl text-[var(--color-text)] md:mt-4 md:text-4xl">
{w.title}
</h3>
<p className="mt-5 text-sm leading-relaxed text-[var(--color-muted)] md:mt-6 md:text-base md:leading-relaxed">
{w.description}
</p>
<p className="mt-5 text-sm font-semibold text-[var(--color-accent)] md:mt-6">
<p className="mt-5 text-sm font-semibold text-[var(--color-accent-deep)] md:mt-6">
{w.hours}
</p>
<ul className="mt-8 grid gap-3 sm:grid-cols-2 md:mt-10">
@ -176,7 +233,7 @@ export default function HomePage() {
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
Explore in 3D
</p>
<h2 className="mt-2 font-display text-3xl md:text-4xl">Virtual experience</h2>
<h2 className="mt-2 font-heading text-3xl md:text-4xl">Virtual experience</h2>
<p className="mt-2 max-w-2xl text-[var(--color-muted)]">
Walk the property before you arrive demo preview below; add a Matterport link in
config when ready.
@ -194,7 +251,7 @@ export default function HomePage() {
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
Our outlets & services
</p>
<h2 className="mt-2 font-display text-3xl md:text-4xl">Dining & venues</h2>
<h2 className="mt-2 font-heading text-3xl md:text-4xl">Dining & venues</h2>
<p className="mt-2 max-w-2xl text-[var(--color-muted)]">
From FeastVille to TABSIA savour flavour, host memorable events, and unwind in
spaces crafted for the city.
@ -208,8 +265,8 @@ export default function HomePage() {
</section>
<section id="meetings" className="mx-auto max-w-7xl px-4 py-16 md:px-8">
<div className="rounded-3xl bg-[var(--color-primary)] px-8 py-14 text-center text-[var(--color-on-primary)] md:px-16">
<h2 className="font-display text-3xl md:text-4xl">Meetings & celebrations</h2>
<div className="rounded-3xl bg-[var(--color-primary)] px-8 py-14 text-center text-[var(--color-on-primary)] shadow-[0_24px_60px_-12px_rgba(0,0,0,0.35)] ring-1 ring-[var(--color-accent)]/25 ring-offset-2 ring-offset-[var(--color-bg)] md:px-16">
<h2 className="font-heading text-3xl md:text-4xl">Meetings & celebrations</h2>
<p className="mx-auto mt-4 max-w-2xl text-sm text-white/85">
Serenity Meeting Room and Fasika Board Room fully equipped for board sessions,
cocktails, and curated catering.
@ -230,7 +287,7 @@ export default function HomePage() {
</div>
<a
href={`mailto:${siteConfig.email}?subject=Event%20inquiry`}
className="mt-8 inline-flex rounded-full bg-white px-8 py-3 text-sm font-semibold text-[var(--color-primary)] transition hover:bg-[var(--color-accent-soft)]"
className="btn-mustard mt-8 inline-flex px-8 py-3 text-sm"
>
Plan an event
</a>
@ -239,7 +296,7 @@ export default function HomePage() {
<section className="border-y border-[var(--color-border)] bg-[var(--color-surface)] py-16">
<div className="mx-auto max-w-7xl px-4 md:px-8">
<h2 className="font-display text-2xl md:text-3xl">All rooms include</h2>
<h2 className="font-heading text-2xl md:text-3xl">All rooms include</h2>
<ul className="mt-8 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{roomAmenities.map((a) => (
<AmenityItem
@ -254,14 +311,11 @@ export default function HomePage() {
</section>
<section className="mx-auto max-w-7xl px-4 py-20 text-center md:px-8">
<h2 className="font-display text-3xl md:text-4xl">Trusted stays. Seamless booking.</h2>
<h2 className="font-heading text-3xl md:text-4xl">Trusted stays. Seamless booking.</h2>
<p className="mx-auto mt-3 max-w-lg text-sm text-[var(--color-muted)]">
Reserve in minutes mock checkout demonstrates the full journey.
</p>
<Link
href="/booking"
className="mt-8 inline-flex rounded-full bg-[var(--color-text)] px-10 py-4 text-sm font-semibold text-white transition hover:bg-[var(--color-primary)]"
>
<Link href="/booking" className="btn-mustard mt-8 inline-flex px-10 py-4 text-sm">
Get started
</Link>
</section>

View File

@ -66,7 +66,7 @@ export function PaymentPageClient() {
return (
<div className="mx-auto max-w-2xl px-4 py-12 md:py-16">
<h1 className="font-display text-3xl">Payment</h1>
<h1 className="font-heading text-3xl">Payment</h1>
<p className="mt-2 text-sm text-[var(--color-muted)]">
Mock form only read our privacy policy before a real launch.
</p>
@ -142,7 +142,7 @@ export function PaymentPageClient() {
<button
type="button"
onClick={applyCoupon}
className="rounded-full border border-[var(--color-primary)] px-4 py-2 text-sm font-semibold text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white"
className="rounded-full border border-[var(--color-primary)] px-4 py-2 text-sm font-semibold text-[var(--color-primary)] transition hover:border-transparent hover:bg-gradient-to-br hover:from-[var(--color-accent-highlight)] hover:via-[var(--color-accent)] hover:to-[var(--color-accent-deep)] hover:text-[var(--color-on-accent)] hover:shadow-[var(--shadow-mustard)]"
>
Apply
</button>
@ -220,7 +220,7 @@ export function PaymentPageClient() {
disabled={loading}
aria-busy={loading}
onClick={handlePay}
className="mt-8 w-full rounded-full bg-[var(--color-text)] py-4 text-sm font-semibold tracking-wide text-white transition hover:bg-[var(--color-primary)] disabled:opacity-60"
className="btn-mustard mt-8 w-full py-4 text-sm tracking-wide disabled:opacity-60"
>
{loading ? "Processing…" : payLabel}
</button>

View File

@ -55,7 +55,7 @@ export default function ReserveHeldPage() {
/>
</svg>
</div>
<h1 className="mt-8 text-center font-display text-3xl md:text-4xl">
<h1 className="mt-8 text-center font-heading text-3xl md:text-4xl">
Reservation on hold
</h1>
<p className="mt-3 text-center text-sm text-[var(--color-muted)]">
@ -95,7 +95,7 @@ export default function ReserveHeldPage() {
<Link
href="/payment"
className="mt-10 flex w-full items-center justify-center rounded-full bg-[var(--color-text)] py-4 text-sm font-semibold text-white transition hover:bg-[var(--color-primary)]"
className="btn-mustard mt-10 flex w-full items-center justify-center py-4 text-sm"
>
Complete payment
</Link>

View File

@ -3,13 +3,13 @@ import Link from "next/link";
export default function RoomNotFound() {
return (
<div className="mx-auto max-w-lg px-4 py-24 text-center">
<h1 className="font-display text-3xl">Room not found</h1>
<h1 className="font-heading text-3xl">Room not found</h1>
<p className="mt-3 text-[var(--color-muted)]">
We couldn&apos;t find that room category.
</p>
<Link
href="/#rooms"
className="mt-8 inline-flex rounded-full bg-[var(--color-primary)] px-8 py-3 text-sm font-semibold text-white"
className="btn-mustard mt-8 inline-flex px-8 py-3 text-sm"
>
View all rooms
</Link>

View File

@ -50,7 +50,7 @@ export default async function RoomPage({ params }: Props) {
>
All rooms
</Link>
<h1 className="mt-2 font-display text-4xl text-white md:text-5xl">{room.name}</h1>
<h1 className="mt-2 font-heading text-4xl text-white md:text-5xl">{room.name}</h1>
<p className="mt-2 max-w-2xl text-sm text-white/90">{room.shortDescription}</p>
</div>
</div>
@ -58,7 +58,7 @@ export default async function RoomPage({ params }: Props) {
<div className="mx-auto max-w-7xl px-4 py-12 md:px-8 md:py-16">
<div className="grid gap-12 lg:grid-cols-[1fr_380px]">
<div>
<h2 className="font-display text-2xl">Overview</h2>
<h2 className="font-heading text-2xl">Overview</h2>
<p className="mt-4 leading-relaxed text-[var(--color-muted)]">{room.longDescription}</p>
<div className="mt-10 grid gap-4 sm:grid-cols-2">
@ -76,7 +76,7 @@ export default async function RoomPage({ params }: Props) {
</div>
<section className="mt-14">
<h2 className="font-display text-2xl">Virtual tour</h2>
<h2 className="font-heading text-2xl">Virtual tour</h2>
<p className="mt-2 text-sm text-[var(--color-muted)]">
Explore this category in 3D demo placeholder until a room-specific embed is
added.
@ -95,7 +95,7 @@ export default async function RoomPage({ params }: Props) {
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-muted)]">
From
</p>
<p className="mt-1 font-display text-3xl text-[var(--color-primary)]">
<p className="mt-1 font-heading text-3xl text-[var(--color-primary)]">
<FormattedUsd amountUsd={room.nightlyRate} />
<span className="text-base font-sans font-normal text-[var(--color-muted)]">
{" "}

View File

@ -18,7 +18,7 @@ export function BookRoomButton({ roomId, className = "" }: Props) {
}}
className={
className ||
"rounded-full bg-[var(--color-text)] px-8 py-3.5 text-sm font-semibold text-white transition hover:bg-[var(--color-primary)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)]"
"btn-mustard px-8 py-3.5 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)]"
}
>
Book this room

View File

@ -13,70 +13,78 @@ export function BookingSearchWidget() {
}, [router]);
return (
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-4 shadow-xl md:p-6">
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-muted)]">
Begin your journey
<div className="overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[0_16px_48px_rgba(23,71,70,0.12)] ring-1 ring-[var(--color-accent)]/15">
<div className="border-l-[4px] border-[var(--color-accent)]/40 bg-gradient-to-br from-[var(--color-accent-soft)] via-[var(--color-surface)] to-[var(--color-surface)] px-5 py-6 shadow-[inset_0_1px_0_rgba(255,255,255,0.65)] sm:px-6 sm:py-7 md:px-8 md:py-8">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-[var(--color-primary)]">
Direct booking
</p>
<div className="mt-4 grid gap-4 md:grid-cols-4 md:items-end">
<h3 className="font-heading mt-3 text-2xl font-semibold leading-snug tracking-tight text-[var(--color-navy)] md:text-3xl">
Reserve your suite in under 2 minutes
</h3>
<p className="mt-3 max-w-xl text-sm leading-relaxed text-[var(--color-muted)]">
Better direct value, fast confirmation, and pay-later flexibility.
</p>
</div>
<div className="border-t border-[var(--color-border)] bg-[var(--color-surface)] px-5 py-6 sm:px-6 md:px-8 md:py-7">
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4 lg:items-end lg:gap-6">
<label className="block text-sm">
<span className="mb-1.5 block font-medium text-[var(--color-text)]">Location</span>
<select
disabled
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3 py-3 text-sm"
aria-label="Location"
>
<option>Addis Ababa</option>
</select>
</label>
<label className="block text-sm">
<span className="mb-1.5 block font-medium text-[var(--color-text)]">Check-in</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-wide text-[var(--color-navy)]">
Check-in
</span>
<input
type="date"
value={checkIn}
onChange={(e) => setDates(e.target.value, checkOut)}
className="w-full rounded-xl border border-[var(--color-border)] px-3 py-3 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20"
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-3 text-sm text-[var(--color-text)] shadow-sm transition focus:border-[var(--color-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/25"
/>
</label>
<label className="block text-sm">
<span className="mb-1.5 block font-medium text-[var(--color-text)]">Check-out</span>
<span className="mb-2 block text-xs font-semibold uppercase tracking-wide text-[var(--color-navy)]">
Check-out
</span>
<input
type="date"
value={checkOut}
onChange={(e) => setDates(checkIn, e.target.value)}
className="w-full rounded-xl border border-[var(--color-border)] px-3 py-3 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20"
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-3 text-sm text-[var(--color-text)] shadow-sm transition focus:border-[var(--color-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/25"
/>
</label>
<div className="flex flex-col gap-2 md:flex-row md:items-end">
<label className="block flex-1 text-sm">
<span className="mb-1.5 block font-medium text-[var(--color-text)]">Guests</span>
<label className="block text-sm">
<span className="mb-2 block text-xs font-semibold uppercase tracking-wide text-[var(--color-navy)]">
Guests
</span>
<input
type="number"
min={1}
max={12}
value={guests}
onChange={(e) => setGuests(Number.parseInt(e.target.value, 10) || 1)}
className="w-full rounded-xl border border-[var(--color-border)] px-3 py-3 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20"
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-3 text-sm text-[var(--color-text)] shadow-sm transition focus:border-[var(--color-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/25"
/>
</label>
<div className="flex flex-col sm:col-span-2 lg:col-span-1">
<span className="mb-2 hidden text-xs font-semibold uppercase tracking-wide text-transparent lg:block">
&nbsp;
</span>
<button
type="button"
onClick={onSearch}
className="h-[46px] shrink-0 rounded-full bg-[var(--color-text)] px-6 text-sm font-semibold text-white transition hover:bg-[var(--color-primary)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)] md:ml-2"
className="h-[46px] w-full rounded-full bg-[var(--color-accent)] px-6 text-sm font-semibold text-[var(--color-on-accent)] shadow-md transition hover:bg-[var(--color-accent-hover)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)]"
>
Search stays
Check availability
</button>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{["Suites", "Studios", "Penthouse", "Meetings"].map((chip) => (
<span
key={chip}
className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3 py-1 text-xs font-medium text-[var(--color-muted)]"
>
<div className="mt-7 flex flex-wrap gap-2.5 border-t border-[var(--color-border)]/80 pt-6">
{["Best-rate direct", "Pay later hold", "Airport shuttle", "Suites"].map((chip) => (
<span key={chip} className="chip-mustard px-3.5 py-1.5">
{chip}
</span>
))}
</div>
</div>
</div>
);
}

View File

@ -9,25 +9,25 @@ export function CallUsFab() {
<div className="pointer-events-none fixed bottom-5 right-5 z-[60] flex flex-col items-end gap-2 md:bottom-8 md:right-8">
<a
href={`mailto:${siteConfig.email}`}
className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-primary)] shadow-lg transition hover:bg-[var(--color-primary)] hover:text-[var(--color-on-primary)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)] md:h-14 md:w-14"
className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-primary)] shadow-lg transition hover:bg-gradient-to-br hover:from-[var(--color-accent-highlight)] hover:to-[var(--color-accent-deep)] hover:text-[var(--color-on-accent)] hover:shadow-[var(--shadow-mustard)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)] md:h-14 md:w-14"
aria-label={`Email ${siteConfig.email}`}
>
<MailIcon />
</a>
<a
href={`tel:${tel}`}
className="group pointer-events-auto flex items-center gap-3 rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] py-3 pl-4 pr-5 text-sm font-semibold text-[var(--color-text)] shadow-lg transition hover:bg-[var(--color-primary)] hover:text-[var(--color-on-primary)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)]"
className="group pointer-events-auto flex items-center gap-3 rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] py-3 pl-4 pr-5 text-sm font-semibold text-[var(--color-text)] shadow-lg transition hover:bg-gradient-to-r hover:from-[var(--color-accent-highlight)] hover:via-[var(--color-accent)] hover:to-[var(--color-accent-deep)] hover:text-[var(--color-on-accent)] hover:shadow-[var(--shadow-mustard)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)]"
aria-label={`Call us at ${siteConfig.primaryPhone}`}
>
<span
className="flex h-10 w-10 items-center justify-center rounded-full bg-[var(--color-primary)] text-white"
className="btn-mustard flex h-10 w-10 items-center justify-center"
aria-hidden
>
<PhoneIcon />
</span>
<span className="flex flex-col leading-tight">
<span>Call us</span>
<span className="text-xs font-normal text-[var(--color-muted)] transition group-hover:text-white/90">
<span className="text-xs font-normal text-[var(--color-muted)] transition group-hover:text-[var(--color-on-accent)]/80">
{siteConfig.primaryPhone}
</span>
</span>

View File

@ -5,32 +5,35 @@ export function Footer() {
return (
<footer
id="contact"
className="border-t border-[var(--color-border)] bg-[var(--color-text)] text-[var(--color-surface-muted)]"
className="border-t border-white/10 bg-[var(--color-navy)] text-stone-200"
>
<div className="mx-auto max-w-7xl px-4 py-16 md:px-8">
<div className="grid gap-12 md:grid-cols-2 lg:grid-cols-12">
<div className="lg:col-span-4">
<p className="font-display text-2xl text-white">{siteConfig.name}</p>
<p className="mt-2 text-sm text-stone-400">{siteConfig.address}</p>
<p className="mt-4 text-sm text-stone-400">{siteConfig.city}, Ethiopia</p>
<p className="font-heading text-2xl text-white">{siteConfig.name}</p>
<p className="mt-2 text-sm text-stone-300">{siteConfig.address}</p>
<p className="mt-4 text-sm text-stone-300">{siteConfig.city}, Ethiopia</p>
</div>
<div className="lg:col-span-3">
<h3 className="text-xs font-semibold uppercase tracking-widest text-stone-500">
<h3 className="text-xs font-semibold uppercase tracking-widest text-stone-400">
Reservations & email
</h3>
<ul className="mt-4 space-y-2 text-sm">
<li>
<a
href={`mailto:${siteConfig.email}`}
className="break-all text-white/90 hover:text-white"
className="break-all text-white hover:text-white"
>
{siteConfig.email}
</a>
</li>
{siteConfig.phones.map((p) => (
<li key={p}>
<a href={`tel:${p.replace(/\s/g, "")}`} className="hover:text-white">
<a
href={`tel:${p.replace(/\s/g, "")}`}
className="text-stone-200 hover:text-white"
>
{p}
</a>
</li>
@ -39,7 +42,7 @@ export function Footer() {
</div>
<div className="lg:col-span-3">
<h3 className="text-xs font-semibold uppercase tracking-widest text-stone-500">
<h3 className="text-xs font-semibold uppercase tracking-widest text-stone-400">
Departments
</h3>
<ul className="mt-4 space-y-4 text-sm">
@ -50,7 +53,7 @@ export function Footer() {
<a
key={p}
href={`tel:${p.replace(/\s/g, "")}`}
className="block text-stone-400 hover:text-white"
className="block text-stone-300 hover:text-white"
>
{p}
</a>
@ -61,7 +64,7 @@ export function Footer() {
</div>
<div className="lg:col-span-2">
<h3 className="text-xs font-semibold uppercase tracking-widest text-stone-500">
<h3 className="text-xs font-semibold uppercase tracking-widest text-stone-400">
Follow us
</h3>
<ul className="mt-4 flex flex-wrap gap-3">
@ -70,7 +73,7 @@ export function Footer() {
href={siteConfig.social.facebook}
target="_blank"
rel="noopener noreferrer"
className="flex h-10 w-10 items-center justify-center rounded-full border border-stone-600 text-white transition hover:border-[var(--color-accent)] hover:text-[var(--color-accent-soft)]"
className="flex h-10 w-10 items-center justify-center rounded-full border border-stone-500 text-white transition hover:border-transparent hover:bg-gradient-to-br hover:from-[var(--color-accent-highlight)] hover:via-[var(--color-accent)] hover:to-[var(--color-accent-deep)] hover:text-[var(--color-on-accent)] hover:shadow-[var(--shadow-mustard)]"
aria-label="Facebook"
>
<IconFacebook />
@ -81,7 +84,7 @@ export function Footer() {
href={siteConfig.social.instagram}
target="_blank"
rel="noopener noreferrer"
className="flex h-10 w-10 items-center justify-center rounded-full border border-stone-600 text-white transition hover:border-[var(--color-accent)] hover:text-[var(--color-accent-soft)]"
className="flex h-10 w-10 items-center justify-center rounded-full border border-stone-500 text-white transition hover:border-transparent hover:bg-gradient-to-br hover:from-[var(--color-accent-highlight)] hover:via-[var(--color-accent)] hover:to-[var(--color-accent-deep)] hover:text-[var(--color-on-accent)] hover:shadow-[var(--shadow-mustard)]"
aria-label="Instagram"
>
<IconInstagram />
@ -92,7 +95,7 @@ export function Footer() {
href={siteConfig.social.twitter}
target="_blank"
rel="noopener noreferrer"
className="flex h-10 w-10 items-center justify-center rounded-full border border-stone-600 text-white transition hover:border-[var(--color-accent)] hover:text-[var(--color-accent-soft)]"
className="flex h-10 w-10 items-center justify-center rounded-full border border-stone-500 text-white transition hover:border-transparent hover:bg-gradient-to-br hover:from-[var(--color-accent-highlight)] hover:via-[var(--color-accent)] hover:to-[var(--color-accent-deep)] hover:text-[var(--color-on-accent)] hover:shadow-[var(--shadow-mustard)]"
aria-label="Twitter / X"
>
<IconTwitter />
@ -103,7 +106,7 @@ export function Footer() {
href={siteConfig.social.whatsapp}
target="_blank"
rel="noopener noreferrer"
className="flex h-10 w-10 items-center justify-center rounded-full border border-stone-600 text-white transition hover:border-[var(--color-accent)] hover:text-[var(--color-accent-soft)]"
className="flex h-10 w-10 items-center justify-center rounded-full border border-stone-500 text-white transition hover:border-transparent hover:bg-gradient-to-br hover:from-[var(--color-accent-highlight)] hover:via-[var(--color-accent)] hover:to-[var(--color-accent-deep)] hover:text-[var(--color-on-accent)] hover:shadow-[var(--shadow-mustard)]"
aria-label="WhatsApp"
>
<IconWhatsApp />
@ -111,32 +114,32 @@ export function Footer() {
</li>
</ul>
<h3 className="mt-8 text-xs font-semibold uppercase tracking-widest text-stone-500">
<h3 className="mt-8 text-xs font-semibold uppercase tracking-widest text-stone-400">
Quick links
</h3>
<ul className="mt-3 space-y-2 text-sm">
<li>
<Link href="/booking" className="hover:text-white">
<Link href="/booking" className="text-stone-200 hover:text-white">
Room booking
</Link>
</li>
<li>
<Link href="/#rooms" className="hover:text-white">
<Link href="/#rooms" className="text-stone-200 hover:text-white">
Rooms
</Link>
</li>
<li>
<Link href="/#wellness" className="hover:text-white">
<Link href="/#wellness" className="text-stone-200 hover:text-white">
Gym & Spa
</Link>
</li>
<li>
<Link href="/#dining" className="hover:text-white">
<Link href="/#dining" className="text-stone-200 hover:text-white">
Dining
</Link>
</li>
<li>
<Link href="/meetings/serenity" className="hover:text-white">
<Link href="/meetings/serenity" className="text-stone-200 hover:text-white">
Serenity meeting room
</Link>
</li>
@ -144,7 +147,7 @@ export function Footer() {
</div>
</div>
<p className="mt-12 border-t border-stone-700 pt-8 text-center text-xs text-stone-500">
<p className="mt-12 border-t border-white/10 pt-8 text-center text-xs text-stone-400">
© {new Date().getFullYear()} Shitaye Suite Hotel. All rights reserved.
</p>
</div>

View File

@ -13,7 +13,7 @@ const nav = [
export function Header() {
return (
<header className="sticky top-0 z-40">
<div className="border-b border-white/10 bg-[var(--color-text)] text-white">
<div className="border-b border-white/10 bg-[var(--color-navy)] text-white">
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-x-3 gap-y-2 px-4 py-2 sm:gap-4 md:px-8">
<CurrencySwitcher />
<div className="flex flex-wrap items-center justify-end gap-1 sm:gap-3">
@ -31,7 +31,7 @@ export function Header() {
<div className="border-b border-[var(--color-border)] bg-[var(--color-surface)]/95 backdrop-blur-md">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-4 md:px-8">
<Link href="/" className="group min-w-0 shrink flex flex-col leading-tight">
<span className="font-display text-lg tracking-tight text-[var(--color-primary)] sm:text-xl md:text-2xl">
<span className="font-nav text-lg tracking-tight text-[var(--color-primary)] sm:text-xl md:text-2xl">
{siteConfig.name}
</span>
<span className="text-[10px] font-medium uppercase tracking-[0.2em] text-[var(--color-muted)] sm:text-[11px]">
@ -46,7 +46,7 @@ export function Header() {
<Link
key={item.href}
href={item.href}
className="transition-colors hover:text-[var(--color-primary)]"
className="transition-colors hover:text-[var(--color-accent-deep)]"
>
{item.label}
</Link>
@ -55,7 +55,7 @@ export function Header() {
<div className="flex shrink-0 items-center">
<Link
href="/booking"
className="rounded-full bg-[var(--color-primary)] px-4 py-2.5 text-sm font-semibold text-[var(--color-on-primary)] shadow-sm transition hover:bg-[var(--color-primary-hover)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)] md:px-5"
className="btn-mustard px-4 py-2.5 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)] md:px-5"
>
Book
</Link>

View File

@ -77,7 +77,7 @@ export function Mock3DPlaceholder({
style={{ transform: "translateZ(-12px)" }}
/>
<div className="relative flex h-full flex-col justify-end p-4 md:p-6">
<p className="font-display text-lg text-[var(--color-primary)] md:text-2xl">
<p className="font-heading text-lg text-[var(--color-primary)] md:text-2xl">
Shitaye
</p>
<p className="text-xs text-[var(--color-muted)]">Virtual walkthrough</p>
@ -85,7 +85,7 @@ export function Mock3DPlaceholder({
</div>
</div>
<div className="pointer-events-none absolute bottom-4 left-4 right-4 flex flex-wrap items-center justify-between gap-2 md:pointer-events-auto">
<span className="rounded-full bg-[var(--color-text)]/85 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
<span className="rounded-full bg-[var(--color-navy)]/85 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
{label}
</span>
{videoTourUrl ? (
@ -93,7 +93,7 @@ export function Mock3DPlaceholder({
href={videoTourUrl}
target="_blank"
rel="noopener noreferrer"
className="pointer-events-auto rounded-full border border-[var(--color-primary)] bg-[var(--color-surface)] px-4 py-1.5 text-xs font-semibold text-[var(--color-primary)] shadow-sm transition hover:bg-[var(--color-primary)] hover:text-[var(--color-on-primary)]"
className="btn-mustard pointer-events-auto px-4 py-1.5 text-xs"
>
Watch video tour
</a>

View File

@ -22,7 +22,7 @@ export function OutletCard({ outlet }: Props) {
{outlet.floor}
</p>
) : null}
<h3 className="font-display text-2xl text-white md:text-3xl">{outlet.name}</h3>
<h3 className="font-heading text-2xl text-white md:text-3xl">{outlet.name}</h3>
<p className="mt-2 max-w-xl text-sm text-white/90">{outlet.tagline}</p>
{outlet.detailHref ? (
<span className="mt-3 inline-block text-xs font-semibold uppercase tracking-wider text-white/90 underline decoration-white/40 underline-offset-4">
@ -34,7 +34,7 @@ export function OutletCard({ outlet }: Props) {
<ul className="space-y-2 border-t border-[var(--color-border)] p-5 text-sm text-[var(--color-muted)] md:p-6">
{outlet.bullets.map((b) => (
<li key={b} className="flex gap-2">
<span className="text-[var(--color-accent)]" aria-hidden>
<span className="text-lg font-bold leading-none text-[var(--color-accent-highlight)]" aria-hidden>
·
</span>
{b}

View File

@ -94,7 +94,7 @@ export function ReviewsMenu({ variant = "default" }: ReviewsMenuProps) {
<BookingDotLogo />
<h2
id="reviews-dialog-title"
className="mt-3 font-display text-lg text-[var(--color-text)] sm:text-xl"
className="mt-3 font-heading text-lg text-[var(--color-text)] sm:text-xl"
>
Guest reviews
</h2>
@ -142,8 +142,8 @@ export function ReviewsMenu({ variant = "default" }: ReviewsMenuProps) {
{r.country} · {r.stayDate}
</p>
</div>
<span className="shrink-0 rounded-md bg-[#003580] px-2 py-0.5 text-xs font-bold text-white">
{r.rating}
<span className="badge-mustard shrink-0 tabular-nums">
{r.rating.toFixed(1)}/{r.maxRating}
</span>
</div>
<p className="mt-2 font-medium text-[var(--color-text)]">{r.title}</p>
@ -160,7 +160,7 @@ export function ReviewsMenu({ variant = "default" }: ReviewsMenuProps) {
href={siteConfig.bookingComReviewsUrl}
target="_blank"
rel="noopener noreferrer"
className="flex w-full flex-col items-center justify-center gap-2 rounded-full bg-[#003580] px-4 py-3 text-center text-sm font-semibold text-white transition hover:bg-[#00224f] sm:flex-row sm:gap-3"
className="btn-mustard flex w-full flex-col items-center justify-center gap-2 px-4 py-3 text-center text-sm sm:flex-row sm:gap-3"
onClick={close}
>
<BookingDotLogo compact />
@ -191,8 +191,8 @@ export function ReviewsMenu({ variant = "default" }: ReviewsMenuProps) {
<span
className={
isTopBar
? "flex items-center gap-1 rounded-md bg-[#003580] px-2 py-0.5 text-xs font-bold text-white"
: "flex items-center gap-1 rounded-md bg-[#003580] px-2 py-1 text-xs font-bold text-white sm:py-0.5"
? "badge-mustard flex items-center gap-1 px-2 py-0.5"
: "badge-mustard flex items-center gap-1 px-2 py-1 sm:py-0.5"
}
>
<span>{overallRatingOutOfFive}</span>
@ -231,12 +231,12 @@ function BookingDotLogo({
}
return (
<div className={`inline-flex items-baseline gap-0 ${className}`}>
<span className="text-xl font-bold tracking-tight text-[#003580] sm:text-2xl">Booking</span>
<span className="text-xl font-bold tracking-tight text-[var(--color-primary)] sm:text-2xl">Booking</span>
<span
className="mx-0.5 inline-block h-2 w-2 shrink-0 translate-y-0.5 rounded-full bg-[#febb02] sm:h-2.5 sm:w-2.5"
aria-hidden
/>
<span className="text-xl font-bold tracking-tight text-[#003580] sm:text-2xl">.com</span>
<span className="text-xl font-bold tracking-tight text-[var(--color-primary)] sm:text-2xl">.com</span>
</div>
);
}

View File

@ -22,7 +22,7 @@ export function RoomCard({ room }: Props) {
</span>
</Link>
<div className="flex flex-1 flex-col p-5 md:p-6">
<h3 className="font-display text-xl text-[var(--color-text)] md:text-2xl">
<h3 className="font-heading text-xl text-[var(--color-text)] md:text-2xl">
<Link href={`/rooms/${room.slug}`} className="hover:text-[var(--color-primary)]">
{room.name}
</Link>
@ -39,7 +39,7 @@ export function RoomCard({ room }: Props) {
</Link>
<Link
href={`/booking?room=${room.id}`}
className="flex h-11 w-11 items-center justify-center rounded-full bg-[var(--color-text)] text-white transition hover:bg-[var(--color-primary)]"
className="btn-mustard flex h-11 w-11 items-center justify-center"
aria-label={`Book ${room.name}`}
>
<span aria-hidden className="text-lg">

27
wrangler.jsonc Normal file
View File

@ -0,0 +1,27 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "shitaye-hotel",
"compatibility_date": "2026-03-17",
"compatibility_flags": [
"nodejs_compat",
"global_fetch_strictly_public"
],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"services": [
{
// Self-reference service binding, the service name must match the worker name
// see https://opennext.js.org/cloudflare/caching
"binding": "WORKER_SELF_REFERENCE",
"service": "shitaye-hotel"
}
],
"images": {
// Enable image optimization
// see https://opennext.js.org/cloudflare/howtos/image
"binding": "IMAGES"
}
}