# API Contract — Genesis Coworking Space frontend

This is the shape the frontend (this repo) requires of the backend project at
`api.genesiscoworkingspace.com.my`. It is derived from `docs/PLANNING.md` §5b/§5c
and mirrored exactly by the zod schemas in `src/api/schemas.ts` — if the two
drift, the frontend will fail to parse real responses and fall back to the
baked snapshot silently (see "Failure behavior" below).

Neither endpoint exists yet. The frontend is fully buildable and launchable
against a mock implementation (`src/api/mock/`) in the meantime — see
`docs/PLANNING.md` §5d.

---

## `GET /spaces/{slug}/gallery` → `SpaceGallery`

`slug` is `genesis` or `hive`.

```ts
type SpaceGallery = {
  slug: 'genesis' | 'hive';
  updatedAt: string; // ISO datetime
  images: GalleryImage[]; // min 1
};

type GalleryImage = {
  id: string;
  alt: string;
  order: number; // integer
  width: number; // intrinsic width of the largest variant, positive integer
  height: number; // intrinsic height, positive integer
  src: string; // JPEG/WebP fallback URL, ~1600w
  variants: {
    avif: { url: string; width: number }[];
    webp: { url: string; width: number }[];
  };
  caption?: string;
};
```

### Requirements, in priority order

1. **Intrinsic `width`/`height` per image, always.** Non-negotiable — the
   frontend computes horizontal-scroller pin distance from these *before* any
   image has loaded. If the CMS stores only a file, it must probe dimensions
   on upload and persist them.
2. **Pre-generated responsive variants.** On upload, derive AVIF and WebP at
   widths `640 / 1024 / 1600 / 2400`, plus one JPEG fallback at 1600w. Cap the
   long edge at 2400px, quality ~72 (AVIF) / ~80 (WebP), strip EXIF. This is
   the single highest-impact item: the space pages are wall-to-wall full-bleed
   photography, and unprocessed multi-MB phone uploads would sink the
   performance budget no matter what the frontend does.
3. **Server-side ordering.** Return `images` already sorted by `order`; the
   frontend sorts defensively but the CMS owns the sequence, because panel
   order is an editorial decision made in the CMS UI.
4. **Editable `alt` text per image,** surfaced as a required field in the CMS
   UI. These are the only images on the site whose alt text isn't in version
   control, and SEO is a commercial priority — leaving them blank forfeits
   image search on the site's most photographic pages.
5. **Immutable, content-hashed asset URLs** served with
   `Cache-Control: public, max-age=31536000, immutable`. The JSON response
   itself: `max-age=300, stale-while-revalidate=86400`.
6. **`updatedAt` on the response** so the frontend's snapshot-refresh script
   and cache invalidation have something to key on.
7. **Public, unauthenticated reads,** origin-restricted by CORS (see below).
   No API key — see the security note.
8. **A stable empty-state answer:** if a space has no images, return `200`
   with the documented shape and an empty array rather than `404`, so the
   frontend distinguishes "no content yet" from "endpoint broken".

---

## `POST /enquiries` → `EnquiryResponse`

Request body:

```ts
type EnquiryInput = {
  name: string;    // 2-100 chars
  phone: string;   // 7-20 chars, digits/spaces/+()-only
  email: string;   // valid email, max 254 chars
  message: string; // 10-2000 chars
  source: 'contact' | 'genesis' | 'hive' | 'home'; // defaults to 'contact'
  website?: string; // honeypot — must arrive empty; treat a filled value as spam
  token?: string;    // captcha token, if issued
};
```

Response (discriminated on `ok`):

```ts
type EnquiryResponse =
  | { ok: true; id: string }
  | { ok: false; error: string; fields?: Record<string, string> };
```

- Validation errors return **HTTP 422** with the `ok: false` shape, `fields`
  keyed to the same field names as `EnquiryInput` so the frontend can attach
  errors to the right inputs.
- Rate limiting, spam filtering, persistence, and the notification email are
  all backend responsibilities.

---

## Security note

A static SPA cannot hold a secret. Any key baked into a Vite build is
readable in devtools by anyone. Therefore:

- CMS reads must be **public and origin-restricted**, not key-authenticated.
- `POST /enquiries` is an **unauthenticated public endpoint** that bots will
  find. The frontend contributes a honeypot field and, if the backend issues
  one, a Turnstile/reCAPTCHA token — neither is real protection on its own;
  rate limiting and server-side validation are load-bearing.

## CORS (docs/PLANNING.md §7 / A8)

- `Access-Control-Allow-Origin` allowlisting `https://genesiscoworkingspace.com.my`,
  `https://www.genesiscoworkingspace.com.my`, and `http://localhost:5173` for dev.
- `POST /enquiries` sends `Content-Type: application/json`, which triggers a
  preflight — `OPTIONS` must be handled and cached via `Access-Control-Max-Age`.
- No cookies, no credentials — requests use `credentials: 'omit'`.

## Failure behavior on the frontend

Every response is parsed through the zod schemas above at the network
boundary (`src/api/schemas.ts`). A response that doesn't match — a null where
a string was promised, a missing field — is caught as a parse error, not a
crash. For the gallery endpoint specifically, a failed fetch or a failed parse
both leave the baked snapshot in place (`docs/PLANNING.md` §5d) — the section
degrades to slightly-stale images, never to an empty or broken page.
