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 # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts 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; 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", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev",
"build": "next build", "build": "next build",
"dev": "next dev",
"start": "next start", "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": { "dependencies": {
"next": "16.2.1", "next": "16.2.1",
@ -14,6 +19,8 @@
"react-dom": "19.2.4" "react-dom": "19.2.4"
}, },
"devDependencies": { "devDependencies": {
"@opennextjs/aws": "^3.9.16",
"@opennextjs/cloudflare": "^1.17.1",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
@ -21,6 +28,7 @@
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.1", "eslint-config-next": "16.2.1",
"tailwindcss": "^4", "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)]"> <p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
Book your stay Book your stay
</p> </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 It only takes a moment
</h1> </h1>
<p className="mt-2 text-sm text-[var(--color-muted)]"> <p className="mt-2 text-sm text-[var(--color-muted)]">
@ -192,7 +192,7 @@ export function BookingPageClient() {
disabled={!canContinue || pending !== null} disabled={!canContinue || pending !== null}
aria-busy={pending === "payment"} aria-busy={pending === "payment"}
onClick={() => placeHold("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"} {pending === "payment" ? "Please wait…" : "Continue to payment"}
</button> </button>
@ -201,7 +201,7 @@ export function BookingPageClient() {
disabled={!canContinue || pending !== null} disabled={!canContinue || pending !== null}
aria-busy={pending === "reserve"} aria-busy={pending === "reserve"}
onClick={() => placeHold("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"} {pending === "reserve" ? "Saving your hold…" : "Reserve now — pay later"}
</button> </button>

View File

@ -39,7 +39,7 @@ export default function ConfirmationPage() {
> >
</div> </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)]"> <p className="mt-3 text-sm text-[var(--color-muted)]">
Thank you, {guest.firstName}. A mock itinerary email would be sent to {guest.email}. Thank you, {guest.firstName}. A mock itinerary email would be sent to {guest.email}.
</p> </p>
@ -92,7 +92,7 @@ export default function ConfirmationPage() {
<Link <Link
href="/" href="/"
onClick={() => resetBooking()} 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 Back to home
</Link> </Link>

View File

@ -1,20 +1,46 @@
@import "tailwindcss"; @import "tailwindcss";
/*
Shitaye Suite Hotel dark teal · mustard gold · warm gray.
Teal: chrome, links, wordmark accents. Mustard: primary buttons & CTAs. Gray: borders / structure.
*/
:root { :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: #ffffff;
--color-surface-muted: #f3ede6; --color-surface-muted: #e8e6e2;
--color-text: #1c1917; --color-iced-mint: #e6ebe9;
--color-muted: #57534e; --color-navy: var(--color-brand-teal);
--color-border: #e7e0d6; --color-text: #1c1c1a;
--color-primary: #7c1d2b; --color-muted: #5c5a54;
--color-primary-hover: #5c1520; --color-border: #a9a79f;
--color-on-primary: #fffaf7; --color-primary: var(--color-brand-teal);
--color-accent: #b8860b; --color-primary-hover: var(--color-brand-teal-hover);
--color-accent-soft: #f5e6c8; --color-on-primary: #f4f5f4;
--color-success: #0d9488; --color-accent: var(--color-brand-mustard);
--font-display: var(--font-cormorant), "Georgia", serif; --color-accent-hover: var(--color-brand-mustard-hover);
--font-ui: var(--font-dm-sans), system-ui, sans-serif; --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 { @theme inline {
@ -33,8 +59,108 @@ body {
font-family: var(--font-ui); font-family: var(--font-ui);
} }
.font-display { /* Headings site-wide (not navbar) */
font-family: var(--font-display); .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 { .grain::before {
@ -54,7 +180,9 @@ body {
} }
.card-lift:hover { .card-lift:hover {
transform: translateY(-4px); 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 { @keyframes mock3d-rotate {

View File

@ -1,23 +1,8 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Cormorant_Garamond, DM_Sans } from "next/font/google";
import { Providers } from "./providers"; import { Providers } from "./providers";
import { Shell } from "@/components/Shell"; import { Shell } from "@/components/Shell";
import "./globals.css"; 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 = { export const metadata: Metadata = {
title: { title: {
default: "Shitaye Suite Hotel | Addis Ababa", default: "Shitaye Suite Hotel | Addis Ababa",
@ -35,9 +20,22 @@ export default function RootLayout({
return ( return (
<html <html
lang="en" lang="en"
className={`${cormorant.variable} ${dmSans.variable} h-full antialiased`} className="h-full antialiased"
> >
<body className="min-h-full"> <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> <Providers>
<Shell>{children}</Shell> <Shell>{children}</Shell>
</Providers> </Providers>

View File

@ -3,10 +3,10 @@ import Link from "next/link";
export default function MeetingNotFound() { export default function MeetingNotFound() {
return ( return (
<div className="mx-auto max-w-lg px-4 py-24 text-center"> <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 <Link
href="/#meetings" 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 View venues
</Link> </Link>

View File

@ -51,7 +51,7 @@ export default async function MeetingSpacePage({ params }: Props) {
> >
Dining & venues Dining & venues
</Link> </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> <p className="mt-2 max-w-2xl text-sm text-white/90">{space.shortDescription}</p>
</div> </div>
</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="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 className="grid gap-12 lg:grid-cols-[1fr_360px]">
<div> <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> <p className="mt-4 leading-relaxed text-[var(--color-muted)]">{space.longDescription}</p>
<div className="mt-10 grid gap-4 sm:grid-cols-2"> <div className="mt-10 grid gap-4 sm:grid-cols-2">
@ -77,7 +77,7 @@ export default async function MeetingSpacePage({ params }: Props) {
</div> </div>
<section className="mt-14"> <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"> <ul className="mt-4 grid gap-2 sm:grid-cols-2">
{space.amenities.map((a) => ( {space.amenities.map((a) => (
<AmenityItem key={a.label} item={a} variant="card" /> <AmenityItem key={a.label} item={a} variant="card" />
@ -86,7 +86,7 @@ export default async function MeetingSpacePage({ params }: Props) {
</section> </section>
<section className="mt-12"> <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"> <ul className="mt-3 flex flex-wrap gap-2">
{space.layouts.map((l) => ( {space.layouts.map((l) => (
<li <li
@ -100,7 +100,7 @@ export default async function MeetingSpacePage({ params }: Props) {
</section> </section>
<section className="mt-12"> <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)]"> <ul className="mt-3 space-y-2 text-sm text-[var(--color-muted)]">
{space.catering.map((c) => ( {space.catering.map((c) => (
<li key={c}>· {c}</li> <li key={c}>· {c}</li>
@ -122,7 +122,7 @@ export default async function MeetingSpacePage({ params }: Props) {
<MeetingHalfDayRate usdAmount={space.halfDayRateUsd} /> <MeetingHalfDayRate usdAmount={space.halfDayRateUsd} />
<a <a
href={`mailto:${siteConfig.email}?subject=${encodeURIComponent(`Event inquiry — ${space.name}`)}`} 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 Request a proposal
</a> </a>

View File

@ -6,6 +6,7 @@ import { OutletCard } from "@/components/OutletCard";
import { RoomCard } from "@/components/RoomCard"; import { RoomCard } from "@/components/RoomCard";
import { VirtualTourBlock } from "@/components/VirtualTourBlock"; import { VirtualTourBlock } from "@/components/VirtualTourBlock";
import { roomAmenities } from "@/lib/mocks/amenities"; import { roomAmenities } from "@/lib/mocks/amenities";
import { bookingStyleReviews } from "@/lib/mocks/bookingReviews";
import { outlets } from "@/lib/mocks/outlets"; import { outlets } from "@/lib/mocks/outlets";
import { rooms } from "@/lib/mocks/rooms"; import { rooms } from "@/lib/mocks/rooms";
import { siteConfig } from "@/lib/mocks/site"; 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="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"> <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"> <p className="text-xs font-semibold uppercase tracking-[0.25em] text-white/80">
Official website Book direct · Shitaye Suite Hotel
</p> </p>
<h1 className="mt-4 max-w-3xl font-display text-4xl font-semibold leading-tight text-white md:text-6xl"> <h1 className="mt-4 max-w-4xl font-heading text-4xl font-semibold leading-tight text-white md:text-6xl">
{siteConfig.tagline} Spacious Luxury Suites Near Bole Airport - Designed for Business & Long Stay
</h1> </h1>
<p className="mt-4 max-w-xl text-lg text-white/90"> <p className="mt-4 max-w-2xl text-lg text-white/90">
Discover refined stays in Addis Ababa exceptional rooms, celebrated dining, and Direct booking benefits, flexible options, and trusted guest-rated hospitality in Addis
spaces designed for connection. Ababa.
</p> </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"> <div className="mt-10 w-full max-w-4xl">
<BookingSearchWidget /> <BookingSearchWidget />
</div> </div>
</div> </div>
</section> </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"> <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 className="grid gap-12 lg:grid-cols-2 lg:items-center">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]"> <p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
About us About us
</p> </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. Geo-convenient. Unmistakably Shitaye.
</h2> </h2>
<p className="mt-4 leading-relaxed text-[var(--color-muted)]"> <p className="mt-4 leading-relaxed text-[var(--color-muted)]">
Close to key places of attraction and major businesses or institutions your base Close to key places of attraction and major businesses or institutions your base
for work, culture, and rest. Begin your journey with us. for work, culture, and rest. Begin your journey with us.
</p> </p>
<Link <Link href="/#rooms" className="btn-mustard mt-6 inline-flex px-6 py-3 text-sm">
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)]"
>
View rooms View rooms
</Link> </Link>
</div> </div>
@ -93,7 +150,7 @@ export default function HomePage() {
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]"> <p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
Stay with us Stay with us
</p> </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)]"> <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 From junior studios to our four-bedroom penthouse every category includes premium
amenities and attentive service. amenities and attentive service.
@ -101,7 +158,7 @@ export default function HomePage() {
</div> </div>
<Link <Link
href="/booking" 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 Book a room
</Link> </Link>
@ -123,7 +180,7 @@ export default function HomePage() {
<p className="text-xs font-semibold uppercase tracking-[0.25em] text-[var(--color-primary)]"> <p className="text-xs font-semibold uppercase tracking-[0.25em] text-[var(--color-primary)]">
Wellness Wellness
</p> </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 Gym & Spa
</h2> </h2>
<p className="mt-5 text-base leading-relaxed text-[var(--color-muted)] md:mt-6 md:text-lg md:leading-relaxed"> <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)]"> <p className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--color-primary)]">
{w.subtitle} {w.subtitle}
</p> </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} {w.title}
</h3> </h3>
<p className="mt-5 text-sm leading-relaxed text-[var(--color-muted)] md:mt-6 md:text-base md:leading-relaxed"> <p className="mt-5 text-sm leading-relaxed text-[var(--color-muted)] md:mt-6 md:text-base md:leading-relaxed">
{w.description} {w.description}
</p> </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} {w.hours}
</p> </p>
<ul className="mt-8 grid gap-3 sm:grid-cols-2 md:mt-10"> <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)]"> <p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
Explore in 3D Explore in 3D
</p> </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)]"> <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 Walk the property before you arrive demo preview below; add a Matterport link in
config when ready. config when ready.
@ -194,7 +251,7 @@ export default function HomePage() {
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]"> <p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-primary)]">
Our outlets & services Our outlets & services
</p> </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)]"> <p className="mt-2 max-w-2xl text-[var(--color-muted)]">
From FeastVille to TABSIA savour flavour, host memorable events, and unwind in From FeastVille to TABSIA savour flavour, host memorable events, and unwind in
spaces crafted for the city. spaces crafted for the city.
@ -208,8 +265,8 @@ export default function HomePage() {
</section> </section>
<section id="meetings" className="mx-auto max-w-7xl px-4 py-16 md:px-8"> <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"> <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-display text-3xl md:text-4xl">Meetings & celebrations</h2> <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"> <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, Serenity Meeting Room and Fasika Board Room fully equipped for board sessions,
cocktails, and curated catering. cocktails, and curated catering.
@ -230,7 +287,7 @@ export default function HomePage() {
</div> </div>
<a <a
href={`mailto:${siteConfig.email}?subject=Event%20inquiry`} 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 Plan an event
</a> </a>
@ -239,7 +296,7 @@ export default function HomePage() {
<section className="border-y border-[var(--color-border)] bg-[var(--color-surface)] py-16"> <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"> <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"> <ul className="mt-8 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{roomAmenities.map((a) => ( {roomAmenities.map((a) => (
<AmenityItem <AmenityItem
@ -254,14 +311,11 @@ export default function HomePage() {
</section> </section>
<section className="mx-auto max-w-7xl px-4 py-20 text-center md:px-8"> <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)]"> <p className="mx-auto mt-3 max-w-lg text-sm text-[var(--color-muted)]">
Reserve in minutes mock checkout demonstrates the full journey. Reserve in minutes mock checkout demonstrates the full journey.
</p> </p>
<Link <Link href="/booking" className="btn-mustard mt-8 inline-flex px-10 py-4 text-sm">
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)]"
>
Get started Get started
</Link> </Link>
</section> </section>

View File

@ -66,7 +66,7 @@ export function PaymentPageClient() {
return ( return (
<div className="mx-auto max-w-2xl px-4 py-12 md:py-16"> <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)]"> <p className="mt-2 text-sm text-[var(--color-muted)]">
Mock form only read our privacy policy before a real launch. Mock form only read our privacy policy before a real launch.
</p> </p>
@ -142,7 +142,7 @@ export function PaymentPageClient() {
<button <button
type="button" type="button"
onClick={applyCoupon} 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 Apply
</button> </button>
@ -220,7 +220,7 @@ export function PaymentPageClient() {
disabled={loading} disabled={loading}
aria-busy={loading} aria-busy={loading}
onClick={handlePay} 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} {loading ? "Processing…" : payLabel}
</button> </button>

View File

@ -55,7 +55,7 @@ export default function ReserveHeldPage() {
/> />
</svg> </svg>
</div> </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 Reservation on hold
</h1> </h1>
<p className="mt-3 text-center text-sm text-[var(--color-muted)]"> <p className="mt-3 text-center text-sm text-[var(--color-muted)]">
@ -95,7 +95,7 @@ export default function ReserveHeldPage() {
<Link <Link
href="/payment" 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 Complete payment
</Link> </Link>

View File

@ -3,13 +3,13 @@ import Link from "next/link";
export default function RoomNotFound() { export default function RoomNotFound() {
return ( return (
<div className="mx-auto max-w-lg px-4 py-24 text-center"> <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)]"> <p className="mt-3 text-[var(--color-muted)]">
We couldn&apos;t find that room category. We couldn&apos;t find that room category.
</p> </p>
<Link <Link
href="/#rooms" 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 View all rooms
</Link> </Link>

View File

@ -50,7 +50,7 @@ export default async function RoomPage({ params }: Props) {
> >
All rooms All rooms
</Link> </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> <p className="mt-2 max-w-2xl text-sm text-white/90">{room.shortDescription}</p>
</div> </div>
</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="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 className="grid gap-12 lg:grid-cols-[1fr_380px]">
<div> <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> <p className="mt-4 leading-relaxed text-[var(--color-muted)]">{room.longDescription}</p>
<div className="mt-10 grid gap-4 sm:grid-cols-2"> <div className="mt-10 grid gap-4 sm:grid-cols-2">
@ -76,7 +76,7 @@ export default async function RoomPage({ params }: Props) {
</div> </div>
<section className="mt-14"> <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)]"> <p className="mt-2 text-sm text-[var(--color-muted)]">
Explore this category in 3D demo placeholder until a room-specific embed is Explore this category in 3D demo placeholder until a room-specific embed is
added. 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)]"> <p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-muted)]">
From From
</p> </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} /> <FormattedUsd amountUsd={room.nightlyRate} />
<span className="text-base font-sans font-normal text-[var(--color-muted)]"> <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={
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 Book this room

View File

@ -13,69 +13,77 @@ export function BookingSearchWidget() {
}, [router]); }, [router]);
return ( return (
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-4 shadow-xl md:p-6"> <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">
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--color-muted)]"> <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">
Begin your journey <p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-[var(--color-primary)]">
</p> Direct booking
<div className="mt-4 grid gap-4 md:grid-cols-4 md:items-end"> </p>
<label className="block text-sm"> <h3 className="font-heading mt-3 text-2xl font-semibold leading-snug tracking-tight text-[var(--color-navy)] md:text-3xl">
<span className="mb-1.5 block font-medium text-[var(--color-text)]">Location</span> Reserve your suite in under 2 minutes
<select </h3>
disabled <p className="mt-3 max-w-xl text-sm leading-relaxed text-[var(--color-muted)]">
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3 py-3 text-sm" Better direct value, fast confirmation, and pay-later flexibility.
aria-label="Location" </p>
> </div>
<option>Addis Ababa</option>
</select> <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">
</label> <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4 lg:items-end lg:gap-6">
<label className="block text-sm"> <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)]">
<input Check-in
type="date" </span>
value={checkIn} <input
onChange={(e) => setDates(e.target.value, checkOut)} type="date"
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" value={checkIn}
/> onChange={(e) => setDates(e.target.value, checkOut)}
</label> 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 className="block text-sm"> />
<span className="mb-1.5 block font-medium text-[var(--color-text)]">Check-out</span> </label>
<input <label className="block text-sm">
type="date" <span className="mb-2 block text-xs font-semibold uppercase tracking-wide text-[var(--color-navy)]">
value={checkOut} Check-out
onChange={(e) => setDates(checkIn, e.target.value)} </span>
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" <input
/> type="date"
</label> value={checkOut}
<div className="flex flex-col gap-2 md:flex-row md:items-end"> onChange={(e) => setDates(checkIn, e.target.value)}
<label className="block flex-1 text-sm"> 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"
<span className="mb-1.5 block font-medium text-[var(--color-text)]">Guests</span> />
</label>
<label className="block text-sm">
<span className="mb-2 block text-xs font-semibold uppercase tracking-wide text-[var(--color-navy)]">
Guests
</span>
<input <input
type="number" type="number"
min={1} min={1}
max={12} max={12}
value={guests} value={guests}
onChange={(e) => setGuests(Number.parseInt(e.target.value, 10) || 1)} 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> </label>
<button <div className="flex flex-col sm:col-span-2 lg:col-span-1">
type="button" <span className="mb-2 hidden text-xs font-semibold uppercase tracking-wide text-transparent lg:block">
onClick={onSearch} &nbsp;
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" </span>
> <button
Search stays type="button"
</button> onClick={onSearch}
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)]"
>
Check availability
</button>
</div>
</div>
<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>
<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)]"
>
{chip}
</span>
))}
</div> </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"> <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 <a
href={`mailto:${siteConfig.email}`} 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}`} aria-label={`Email ${siteConfig.email}`}
> >
<MailIcon /> <MailIcon />
</a> </a>
<a <a
href={`tel:${tel}`} 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}`} aria-label={`Call us at ${siteConfig.primaryPhone}`}
> >
<span <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 aria-hidden
> >
<PhoneIcon /> <PhoneIcon />
</span> </span>
<span className="flex flex-col leading-tight"> <span className="flex flex-col leading-tight">
<span>Call us</span> <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} {siteConfig.primaryPhone}
</span> </span>
</span> </span>

View File

@ -5,32 +5,35 @@ export function Footer() {
return ( return (
<footer <footer
id="contact" 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="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="grid gap-12 md:grid-cols-2 lg:grid-cols-12">
<div className="lg:col-span-4"> <div className="lg:col-span-4">
<p className="font-display text-2xl text-white">{siteConfig.name}</p> <p className="font-heading text-2xl text-white">{siteConfig.name}</p>
<p className="mt-2 text-sm text-stone-400">{siteConfig.address}</p> <p className="mt-2 text-sm text-stone-300">{siteConfig.address}</p>
<p className="mt-4 text-sm text-stone-400">{siteConfig.city}, Ethiopia</p> <p className="mt-4 text-sm text-stone-300">{siteConfig.city}, Ethiopia</p>
</div> </div>
<div className="lg:col-span-3"> <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 Reservations & email
</h3> </h3>
<ul className="mt-4 space-y-2 text-sm"> <ul className="mt-4 space-y-2 text-sm">
<li> <li>
<a <a
href={`mailto:${siteConfig.email}`} href={`mailto:${siteConfig.email}`}
className="break-all text-white/90 hover:text-white" className="break-all text-white hover:text-white"
> >
{siteConfig.email} {siteConfig.email}
</a> </a>
</li> </li>
{siteConfig.phones.map((p) => ( {siteConfig.phones.map((p) => (
<li key={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} {p}
</a> </a>
</li> </li>
@ -39,7 +42,7 @@ export function Footer() {
</div> </div>
<div className="lg:col-span-3"> <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 Departments
</h3> </h3>
<ul className="mt-4 space-y-4 text-sm"> <ul className="mt-4 space-y-4 text-sm">
@ -50,7 +53,7 @@ export function Footer() {
<a <a
key={p} key={p}
href={`tel:${p.replace(/\s/g, "")}`} href={`tel:${p.replace(/\s/g, "")}`}
className="block text-stone-400 hover:text-white" className="block text-stone-300 hover:text-white"
> >
{p} {p}
</a> </a>
@ -61,7 +64,7 @@ export function Footer() {
</div> </div>
<div className="lg:col-span-2"> <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 Follow us
</h3> </h3>
<ul className="mt-4 flex flex-wrap gap-3"> <ul className="mt-4 flex flex-wrap gap-3">
@ -70,7 +73,7 @@ export function Footer() {
href={siteConfig.social.facebook} href={siteConfig.social.facebook}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" aria-label="Facebook"
> >
<IconFacebook /> <IconFacebook />
@ -81,7 +84,7 @@ export function Footer() {
href={siteConfig.social.instagram} href={siteConfig.social.instagram}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" aria-label="Instagram"
> >
<IconInstagram /> <IconInstagram />
@ -92,7 +95,7 @@ export function Footer() {
href={siteConfig.social.twitter} href={siteConfig.social.twitter}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" aria-label="Twitter / X"
> >
<IconTwitter /> <IconTwitter />
@ -103,7 +106,7 @@ export function Footer() {
href={siteConfig.social.whatsapp} href={siteConfig.social.whatsapp}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" aria-label="WhatsApp"
> >
<IconWhatsApp /> <IconWhatsApp />
@ -111,32 +114,32 @@ export function Footer() {
</li> </li>
</ul> </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 Quick links
</h3> </h3>
<ul className="mt-3 space-y-2 text-sm"> <ul className="mt-3 space-y-2 text-sm">
<li> <li>
<Link href="/booking" className="hover:text-white"> <Link href="/booking" className="text-stone-200 hover:text-white">
Room booking Room booking
</Link> </Link>
</li> </li>
<li> <li>
<Link href="/#rooms" className="hover:text-white"> <Link href="/#rooms" className="text-stone-200 hover:text-white">
Rooms Rooms
</Link> </Link>
</li> </li>
<li> <li>
<Link href="/#wellness" className="hover:text-white"> <Link href="/#wellness" className="text-stone-200 hover:text-white">
Gym & Spa Gym & Spa
</Link> </Link>
</li> </li>
<li> <li>
<Link href="/#dining" className="hover:text-white"> <Link href="/#dining" className="text-stone-200 hover:text-white">
Dining Dining
</Link> </Link>
</li> </li>
<li> <li>
<Link href="/meetings/serenity" className="hover:text-white"> <Link href="/meetings/serenity" className="text-stone-200 hover:text-white">
Serenity meeting room Serenity meeting room
</Link> </Link>
</li> </li>
@ -144,7 +147,7 @@ export function Footer() {
</div> </div>
</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. © {new Date().getFullYear()} Shitaye Suite Hotel. All rights reserved.
</p> </p>
</div> </div>

View File

@ -13,7 +13,7 @@ const nav = [
export function Header() { export function Header() {
return ( return (
<header className="sticky top-0 z-40"> <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"> <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 /> <CurrencySwitcher />
<div className="flex flex-wrap items-center justify-end gap-1 sm:gap-3"> <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="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"> <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"> <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} {siteConfig.name}
</span> </span>
<span className="text-[10px] font-medium uppercase tracking-[0.2em] text-[var(--color-muted)] sm:text-[11px]"> <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 <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
className="transition-colors hover:text-[var(--color-primary)]" className="transition-colors hover:text-[var(--color-accent-deep)]"
> >
{item.label} {item.label}
</Link> </Link>
@ -55,7 +55,7 @@ export function Header() {
<div className="flex shrink-0 items-center"> <div className="flex shrink-0 items-center">
<Link <Link
href="/booking" 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 Book
</Link> </Link>

View File

@ -77,7 +77,7 @@ export function Mock3DPlaceholder({
style={{ transform: "translateZ(-12px)" }} style={{ transform: "translateZ(-12px)" }}
/> />
<div className="relative flex h-full flex-col justify-end p-4 md:p-6"> <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 Shitaye
</p> </p>
<p className="text-xs text-[var(--color-muted)]">Virtual walkthrough</p> <p className="text-xs text-[var(--color-muted)]">Virtual walkthrough</p>
@ -85,7 +85,7 @@ export function Mock3DPlaceholder({
</div> </div>
</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"> <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} {label}
</span> </span>
{videoTourUrl ? ( {videoTourUrl ? (
@ -93,7 +93,7 @@ export function Mock3DPlaceholder({
href={videoTourUrl} href={videoTourUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" 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 Watch video tour
</a> </a>

View File

@ -22,7 +22,7 @@ export function OutletCard({ outlet }: Props) {
{outlet.floor} {outlet.floor}
</p> </p>
) : null} ) : 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> <p className="mt-2 max-w-xl text-sm text-white/90">{outlet.tagline}</p>
{outlet.detailHref ? ( {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"> <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"> <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) => ( {outlet.bullets.map((b) => (
<li key={b} className="flex gap-2"> <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> </span>
{b} {b}

View File

@ -94,7 +94,7 @@ export function ReviewsMenu({ variant = "default" }: ReviewsMenuProps) {
<BookingDotLogo /> <BookingDotLogo />
<h2 <h2
id="reviews-dialog-title" 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 Guest reviews
</h2> </h2>
@ -142,8 +142,8 @@ export function ReviewsMenu({ variant = "default" }: ReviewsMenuProps) {
{r.country} · {r.stayDate} {r.country} · {r.stayDate}
</p> </p>
</div> </div>
<span className="shrink-0 rounded-md bg-[#003580] px-2 py-0.5 text-xs font-bold text-white"> <span className="badge-mustard shrink-0 tabular-nums">
{r.rating} {r.rating.toFixed(1)}/{r.maxRating}
</span> </span>
</div> </div>
<p className="mt-2 font-medium text-[var(--color-text)]">{r.title}</p> <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} href={siteConfig.bookingComReviewsUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} onClick={close}
> >
<BookingDotLogo compact /> <BookingDotLogo compact />
@ -191,8 +191,8 @@ export function ReviewsMenu({ variant = "default" }: ReviewsMenuProps) {
<span <span
className={ className={
isTopBar isTopBar
? "flex items-center gap-1 rounded-md bg-[#003580] px-2 py-0.5 text-xs font-bold text-white" ? "badge-mustard flex items-center gap-1 px-2 py-0.5"
: "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-1 sm:py-0.5"
} }
> >
<span>{overallRatingOutOfFive}</span> <span>{overallRatingOutOfFive}</span>
@ -231,12 +231,12 @@ function BookingDotLogo({
} }
return ( return (
<div className={`inline-flex items-baseline gap-0 ${className}`}> <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 <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" 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 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> </div>
); );
} }

View File

@ -22,7 +22,7 @@ export function RoomCard({ room }: Props) {
</span> </span>
</Link> </Link>
<div className="flex flex-1 flex-col p-5 md:p-6"> <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)]"> <Link href={`/rooms/${room.slug}`} className="hover:text-[var(--color-primary)]">
{room.name} {room.name}
</Link> </Link>
@ -39,7 +39,7 @@ export function RoomCard({ room }: Props) {
</Link> </Link>
<Link <Link
href={`/booking?room=${room.id}`} 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}`} aria-label={`Book ${room.name}`}
> >
<span aria-hidden className="text-lg"> <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"
}
}