Add a self-contained demo build target for the Providers → Packages → Comparison flow, deployable as a static SPA at /arrangement/. - vite.demo.config.ts: per-slice build via --mode, base path flips for dev vs prod, output to dist-demo/<slice>/ - src/demo/: shared fixtures (7 providers across verified/tier3/tier2 with real venue photography from brandassets) + Zustand basket store with ?compare= URL persistence - Verified-provider packages now share the nine canonical Essentials line items per FA convention; only Optionals/Extras vary - App-level CompareBar surfaces "Already added" / "Maximum 3" feedback via transient store error - ProviderCard logo objectFit cover→contain so wide logos don't crop - npm scripts demo:dev / demo:build, deps zustand + react-router-dom
219 lines
9.2 KiB
Markdown
219 lines
9.2 KiB
Markdown
# Client demo hosting plan
|
|
|
|
**Status:** scoped, not implemented.
|
|
**Target:** share self-contained, interactive demos of the FA design system with clients via `parsons.tensordesign.com.au/<slice>`, gated behind basic auth, independently buildable per slice.
|
|
|
|
---
|
|
|
|
## Why not Storybook or Chromatic alone
|
|
|
|
- **Storybook** — great for isolated components and per-story state, but cross-page flows with persistent state (comparison basket, map selections, route navigation) are outside its shape. You can fake flows with story parameters, but it's brittle and doesn't feel like a product.
|
|
- **Chromatic** — built for visual regression diffs + internal review. UX is Storybook-shaped, which is noisy for non-technical clients. Keep Chromatic for the internal review workflow; use self-hosted demos for client-facing previews.
|
|
|
|
The demo-hosting solution is **additive** — it doesn't replace either of those.
|
|
|
|
---
|
|
|
|
## Goals
|
|
|
|
1. Multiple demo "slices" per project — e.g. `/arrangement`, `/home`, `/compare` — each independently buildable and deployable.
|
|
2. Each slice behaves like a real product: real URLs, real navigation, real state (comparison basket persists across pages, selections survive drill-in, etc.).
|
|
3. Dummy data only — no CMS, no real users, no auth beyond a single shared htpasswd for the whole demo host.
|
|
4. Zero disruption to the component library — demos consume the existing page components as-is.
|
|
5. Demos live alongside the codebase but build into their own output tree (`dist-demo/<slice>/`) so neither the component library nor Storybook is affected.
|
|
|
|
---
|
|
|
|
## URL shape
|
|
|
|
**Single subdomain, subpath per slice:**
|
|
|
|
- `parsons.tensordesign.com.au/` → tiny index page listing available demos (optional, but handy once there are 3+)
|
|
- `parsons.tensordesign.com.au/arrangement` → Providers → Packages → Comparison flow
|
|
- `parsons.tensordesign.com.au/home` → homepage exploration
|
|
- `parsons.tensordesign.com.au/<future slice>`
|
|
|
|
One SSL cert (Let's Encrypt), one nginx server block, one htpasswd covering the whole host.
|
|
|
|
**Why subpath-per-slice over subdomain-per-slice:** subdomain-per-slice needs wildcard DNS + wildcard cert + per-demo nginx blocks. More moving parts for no client-facing benefit. Subpath works cleanly when each Vite build declares its own `base` at build time.
|
|
|
|
**Why this host over a wildcard demo subdomain:** `parsons.tensordesign.com.au` reads clearly to clients ("this is the Parsons project"). If you later run demos for other clients, add siblings (`acme.tensordesign.com.au`) rather than splitting Parsons across multiple hosts.
|
|
|
|
---
|
|
|
|
## Project structure
|
|
|
|
```
|
|
src/demo/
|
|
shared/
|
|
fixtures/ # cross-slice mock data (providers, packages, venues)
|
|
state/ # common stores — comparison basket, nav shell
|
|
theme/ # ThemeProvider wrapper (mirrors Storybook decorators)
|
|
apps/
|
|
arrangement/
|
|
main.tsx # Vite entry
|
|
App.tsx # Router shell
|
|
routes/
|
|
providers.tsx
|
|
packages.tsx
|
|
comparison.tsx
|
|
fixtures/ # arrangement-specific overrides
|
|
home/
|
|
main.tsx
|
|
App.tsx
|
|
routes/
|
|
landing.tsx
|
|
index-demo.html # template — slot in slice-specific title/base
|
|
|
|
scripts/
|
|
build-demo.sh <name> # vite build -c vite.demo.config.ts --mode <name>
|
|
deploy-demo.sh <name> # rsync dist-demo/<name>/ to server path
|
|
|
|
vite.demo.config.ts # reads slice name from --mode, sets base + outDir
|
|
|
|
docs/reference/
|
|
client-demo-hosting-plan.md # this file
|
|
client-demo-deploy.md # once implemented — ops runbook
|
|
```
|
|
|
|
**Fixture-sharing rule:** anything that could plausibly appear in two slices lives in `src/demo/shared/fixtures/`. Slice-specific overrides live in `apps/<slice>/fixtures/`. Stories continue to use their own fixtures unchanged — no cross-contamination.
|
|
|
|
---
|
|
|
|
## State shape (the one thing worth being deliberate about)
|
|
|
|
The comparison basket is cross-page state. Whatever we pick here will likely inform the real app's state layer later, so treat it as the prototype for production, not throwaway glue.
|
|
|
|
**Recommended: Zustand with URL-persistence for package IDs.**
|
|
|
|
```ts
|
|
// src/demo/shared/state/useComparisonBasket.ts
|
|
interface ComparisonBasket {
|
|
packageIds: string[]; // ordered — insertion order = display order
|
|
add: (id: string) => void;
|
|
remove: (id: string) => void;
|
|
clear: () => void;
|
|
isFull: () => boolean; // 4 max per FA convention
|
|
}
|
|
```
|
|
|
|
- **Zustand** over Context: less boilerplate, better Devtools story, selector-based subscriptions avoid re-render cascades.
|
|
- **URL-persistence** (`?compare=a,b,c`) so a client can bookmark a specific comparison and reload. Easy `useEffect` hook that syncs store ↔ URL search param.
|
|
- **Insertion order preserved** — matches ComparisonPage columns left-to-right.
|
|
- **No localStorage** — keeps demo stateless between sessions unless client explicitly shares a URL. Makes client demos predictable ("click here, you'll see exactly what I saw").
|
|
|
|
Other state that might need a shared store: selected provider (persists across routes), map viewport (so map doesn't reset on drill-in). Start with just the basket; add others when a route actually needs them.
|
|
|
|
---
|
|
|
|
## Vite build
|
|
|
|
One config file, slice name passed via `--mode`:
|
|
|
|
```ts
|
|
// vite.demo.config.ts
|
|
export default defineConfig(({ mode }) => ({
|
|
root: `src/demo/apps/${mode}`,
|
|
base: `/${mode}/`,
|
|
build: {
|
|
outDir: `../../../../dist-demo/${mode}`,
|
|
emptyOutDir: true,
|
|
},
|
|
// ... shared plugins, resolve aliases identical to main vite.config
|
|
}));
|
|
```
|
|
|
|
`npm run build:demo -- --mode arrangement` produces `dist-demo/arrangement/` ready to ship.
|
|
|
|
**Shared scripts:**
|
|
```json
|
|
"scripts": {
|
|
"demo:dev": "vite -c vite.demo.config.ts",
|
|
"demo:build": "vite build -c vite.demo.config.ts",
|
|
"demo:deploy": "./scripts/deploy-demo.sh"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Deploy
|
|
|
|
```bash
|
|
# scripts/deploy-demo.sh <slice>
|
|
SLICE=$1
|
|
rsync -az --delete dist-demo/$SLICE/ richie@tensordesign.com.au:/var/www/parsons-demos/$SLICE/
|
|
```
|
|
|
|
Slice-by-slice deploy means polishing one demo doesn't require rebuilding or redeploying others. Add the index page as a trivial static HTML (no build) committed to `scripts/demo-index.html` and rsynced to the host root.
|
|
|
|
Consider a tiny pre-flight check in the deploy script — abort if the build output is missing or empty, to avoid rsync-ing a half-built bundle over a working demo.
|
|
|
|
---
|
|
|
|
## nginx + basic auth
|
|
|
|
```nginx
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name parsons.tensordesign.com.au;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/parsons.tensordesign.com.au/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/parsons.tensordesign.com.au/privkey.pem;
|
|
|
|
root /var/www/parsons-demos;
|
|
index index.html;
|
|
|
|
auth_basic "Parsons demos";
|
|
auth_basic_user_file /etc/nginx/htpasswd/parsons;
|
|
|
|
# SPA fallback per slice — each demo has its own index.html
|
|
location ~ ^/(?<slice>[^/]+)/ {
|
|
try_files $uri $uri/ /$slice/index.html;
|
|
}
|
|
|
|
# Root lists available demos
|
|
location = / {
|
|
try_files /index.html =404;
|
|
}
|
|
}
|
|
```
|
|
|
|
htpasswd setup:
|
|
```bash
|
|
sudo htpasswd -c /etc/nginx/htpasswd/parsons client
|
|
```
|
|
|
|
Single credential shared across all slices. If a client project ever needs isolated access, split at the location block with per-slice `auth_basic_user_file`.
|
|
|
|
---
|
|
|
|
## Rollout order (next session)
|
|
|
|
1. **Scaffold** `src/demo/shared/` (fixtures extracted from `PackagesStep.stories.tsx` + `ComparisonPage.stories.tsx`) and the Zustand basket store.
|
|
2. **First slice — `arrangement`** with three routes (Providers → Packages → Comparison). Prove the basket persists across navigation, the back button works, drill-in still fires on mobile.
|
|
3. **`vite.demo.config.ts` + `build-demo.sh`** — confirm `dist-demo/arrangement/` builds and serves standalone via `npx serve dist-demo/arrangement`.
|
|
4. **nginx + htpasswd + Let's Encrypt** on the server. One-time ops setup.
|
|
5. **Deploy script** — rsync wired to a single command.
|
|
6. **Test end-to-end** — visit `parsons.tensordesign.com.au/arrangement`, click through the flow, verify basket state, verify URL bookmark restores state.
|
|
7. **Optional: index page + second slice** once the first is proven.
|
|
|
|
Estimated effort: half-day scaffold + ~day for the arrangement slice + half-day ops setup. Total ~2 days before the first client-shareable link.
|
|
|
|
---
|
|
|
|
## Non-goals (for this iteration)
|
|
|
|
- No real backend, no CMS, no user accounts, no persistence beyond URL params.
|
|
- No analytics or telemetry (demos are short-lived; instrumenting them is noise).
|
|
- No E2E tests for demo routes (fixtures + trusted component library = low ROI).
|
|
- No automated deploy on merge — manual `npm run demo:deploy arrangement` is fine at this volume. Revisit if demos are updated daily.
|
|
- No custom domain per client — stick with subpath-per-slice under `parsons.tensordesign.com.au`.
|
|
|
|
---
|
|
|
|
## Open questions for next session
|
|
|
|
- Exact path for the server-side document root (`/var/www/parsons-demos/` is a placeholder — confirm what tensordesign.com.au's existing nginx expects).
|
|
- Whether to seed the `home` slice or a different one as the second demo after `arrangement` is proven.
|
|
- Whether the comparison basket should persist across browser tabs (probably no — URL-only is cleaner for demos) or across reloads without a URL (probably no for the same reason).
|