# Demo app

URL: https://developers.uidu.org/docs/recipes/demo-app

> A complete Next.js 16 example exercising every domain in @uidu/client.

The repo ships with a complete Next.js 16 demo at [`apps/demo/`](https://github.com/uidu-org/api.js/tree/main/apps/demo).

**Try it live: [api-js-demo.vercel.app](https://api-js-demo.vercel.app)** — it reads from the seeded `demo` workspace on staging (`demo.uidu.dev`).

## What it covers

| Route                        | What it shows                                                |
| ---------------------------- | ------------------------------------------------------------ |
| `/`                          | CMS landing page rendered block by block with `<PageBlocks>` |
| `/events`, `/events/[id]`    | Event listing and detail, with RSVP                          |
| `/stories`, `/stories/[id]`  | Stories (blog) listing and detail                            |
| `/careers`, `/careers/[id]`  | Job openings and the application flow                        |
| `/forms`, `/forms/[id]`      | Any uidu form rendered with `<DynamicForm>`                  |
| `/help`, `/help/[articleId]` | Help center channel, collections and articles                |
| `/donate`, `/donate/[id]`    | Donation campaigns                                           |
| `/bookings`, `/courses`      | Calendars and bookings, courses and lectures                 |
| `/store`, `/memberships`     | Storefront and membership UI patterns                        |
| `/custom-app`                | A custom app built on the same client                        |
| `/admin/*`                   | Employees, schedule, timesheet, CRM, campaigns, tasks, sites |

Where a workspace has no data for a domain, a few pages fall back to sample data so the UI is still visible.

<Callout type="info">
  The live demo runs without an API key, so it is **read-only**: form
  submissions, RSVPs and the `/admin` pages need an API key, which the public
  deploy does not have. Run it locally with `UIDU_API_KEY` set to try them.
</Callout>

## Run it locally

```bash
# from the repo root
cp apps/demo/.env.example apps/demo/.env.local
# defaults point at the staging `demo` workspace
yarn install
yarn workspace demo dev
```

Open [http://localhost:3002](http://localhost:3002).

## Configuration

Everything is driven by env vars (see `apps/demo/.env.example`):

| Variable                             | Default    | Purpose                                                                          |
| ------------------------------------ | ---------- | -------------------------------------------------------------------------------- |
| `UIDU_HOST`                          | `uidu.dev` | Which uidu to talk to. Staging by default; set `uidu.org` to opt into production |
| `UIDU_WORKSPACE`                     | `demo`     | Workspace subdomain                                                              |
| `UIDU_PUBLIC_TOKEN`                  | —          | Optional public read token                                                       |
| `UIDU_API_KEY`                       | —          | Server-only Bearer token for mutations and `/admin`                              |
| `UIDU_LANDING_SLUG`                  | `landing`  | CMS page rendered on `/`                                                         |
| `UIDU_PROJECT_ID`, `UIDU_CHANNEL_ID` | resolved   | Pin a specific CMS project / help-center channel                                 |

The CMS project and the help-center channel are resolved from the workspace at runtime, so nothing workspace-specific is committed.

## How it's built

### `lib/uidu.ts` — one client

```ts
import { createClient } from '@uidu/client';

const host = process.env.UIDU_HOST ?? 'uidu.dev';
const workspace = process.env.UIDU_WORKSPACE ?? 'demo';

export const uidu = createClient({
  workspace,
  endpoint: `https://${workspace}.${host}/graphql`,
  publicToken: process.env.UIDU_PUBLIC_TOKEN,
  apiKey: process.env.UIDU_API_KEY,
});
```

One client, used from every Server Component and Server Action. No provider needed on the server.

### Landing page — CMS to JSX

```tsx
// app/page.tsx
import { getPage } from '@uidu/client';
import { PageBlocks } from '@uidu/react';
import { ContactForm } from '@/components/blocks/ContactForm';
import { Feature } from '@/components/blocks/Feature';
import { Hero } from '@/components/blocks/Hero';
import { config, defaultProjectId, uidu } from '@/lib/uidu';

export default async function HomePage() {
  const projectId = await defaultProjectId();
  const page = projectId
    ? await getPage(uidu, { projectId, slug: config.landingSlug })
    : null;
  if (!page) return <NotFound />;

  return (
    <PageBlocks
      pageBlocks={page.pageBlocks}
      components={{ Header: Hero, feature1: Feature, ModuloContatto: ContactForm }}
    />
  );
}
```

`<PageBlocks>` walks the blocks, matches each block's shortname against the components map, and renders it. Block components read their fields with `useFields(block.fieldValues)` and `toText()`.

### Contact form — Server Action mutation

The `ModuloContatto` block carries a linked `Form` record. The block hands it to a `'use client'` component that submits through a Server Action:

```ts
// lib/actions.ts
'use server';
import { createFormResponse } from '@uidu/client';
import { uidu } from './uidu';

export async function submitContactForm(formId, fieldValues, contact) {
  const result = await createFormResponse(uidu, {
    input: {
      attributes: { formId, fieldValuesAttributes: /* … */, contactAttributes: /* … */ },
      event: 'complete!',
    },
  });
  return result?.errors?.length
    ? { ok: false, errors: result.errors }
    : { ok: true, responseId: result.formResponse.id };
}
```

### Help center — one query, full tree

```tsx
// app/help/page.tsx
const channelId = await defaultChannelId();
const channel = channelId ? await getChannel(uidu, { id: channelId }) : null;
```

One `getChannel` call returns the channel with its collections and articles; the page renders the tree without further fetches.

## Files at a glance

```
apps/demo/src/
├── app/
│   ├── page.tsx              # Landing — getPage() + <PageBlocks>
│   ├── events/ stories/ careers/ forms/ help/ donate/ …
│   └── admin/                # API-key pages
├── components/
│   ├── Nav.tsx
│   ├── blocks/               # Hero, Feature, ContactForm (+ client part)
│   ├── themes/               # per-section layout variants
│   └── ui/                   # shadcn primitives
└── lib/
    ├── uidu.ts               # createClient() + project/channel resolution
    └── actions.ts            # Server Actions (forms, RSVP, applications, …)
```

## Patterns to take away

1. **One client instance per app.** Server-side, no providers. Pass it to every domain function.
2. **CMS blocks → component map.** The CMS holds the shape, your code holds the components.
3. **Server Components for fetching, Server Actions for mutating, Client Components for interactivity.**
4. **Linked records.** When a block embeds a record (a Form inside a page block), the typed response exposes it inline — no second round trip.
5. **Keep secrets server-side.** `UIDU_API_KEY` is only read in Server Components and Server Actions, never shipped to the browser.
