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:
parent
0b7c0fcd2b
commit
1a37b91795
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -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
220
ADMIN_BOOKING_README.md
Normal 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 | 1–12 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 mock’s `payLaterHold` to your policy: e.g. **held unpaid** vs **held with intent to pay later**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Mock API payloads (prototypes for real endpoints)
|
||||||
|
|
||||||
|
Today’s 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
|
||||||
|
|
||||||
|
- Today’s 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.*
|
||||||
|
|
@ -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
9
open-next.config.ts
Normal 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
5624
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
|
|
@ -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
4
public/_headers
Normal 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
|
||||||
41
scripts/patch-open-next-handler.js
Normal file
41
scripts/patch-open-next-handler.js
Normal 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);
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
108
src/app/page.tsx
108
src/app/page.tsx
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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't find that room category.
|
We couldn'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>
|
||||||
|
|
|
||||||
|
|
@ -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)]">
|
||||||
{" "}
|
{" "}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
27
wrangler.jsonc
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user