Demo page: the running product beside the case for it

A page to present this to a client. Left column is the real app in the phone
frame — live, swipeable, the same deck and the same data, not a screenshot.
Right column is what somebody needs in order to judge it: how to get in, what it
does, what it is built on.

Credentials, both sides
- A one-sided demo of a two-sided marketplace shows half a product, so the panel
  carries a customer AND a tradesperson, each with a copy button. Reading a
  phone number off a screen into a form while a client watches is a small
  humiliation; mistyping one is a worse one.
- dev-login grows from one pinned number to a named DEMO_ACCOUNTS list. The
  three guards are unchanged — not production, explicitly enabled, and the
  number must be on the list. Both accounts map to seeded users that already own
  jobs, conversations and a completed booking, so the screens have something in
  them rather than five empty states.
- The page SAYS SO when sign-in is unavailable rather than letting somebody
  discover it mid-meeting. A fixed passcode is a login bypass and must never
  ship live; the honest fix for demoing against production is a real account and
  a real SMS, not a fourth flag.

Content lives in arrays at the top of demo-panel, so adding a feature or
swapping a dependency is one line and the layout is untouched. Four labelled
placeholder slots — roadmap, pricing, metrics, case study — reserve the space
and make it obvious what belongs where.

A deliberate exception to DESIGN.md §1.0 and §4, which ban desktop layouts and
marketing pages, and it is noted as one in the file. This is a frame around the
running app for a laptop, not a product screen; the app inside is untouched and
still mobile-only. Below `lg` the panel stacks under the phone, because a client
who opens the link on their own phone should still be able to read it.

Verified: both demo accounts complete the real OTP path end to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
serfa
2026-08-21 07:31:16 -04:00
co-authored by Claude Opus 5
parent 0c49aa9502
commit 7567aa4e75
6 changed files with 438 additions and 59 deletions
+58 -13
View File
@@ -1,31 +1,74 @@
import { eq } from 'drizzle-orm';
import { inArray } from 'drizzle-orm';
import { db, schema } from '@linkder/db';
/**
* A fixed test account for local development.
* Fixed sign-in for local development and for demonstrating the product.
*
* Signing in normally needs a real handset to receive a real SMS, which makes
* the whole app untestable without a phone in your hand and Twilio credits. This
* pins one number to one known code so `pnpm dev` is usable.
* the whole app untestable without a phone in your hand and Twilio credits — and
* makes it impossible to show somebody both sides of a two-sided marketplace in
* a meeting. These accounts pin known numbers to one known code.
*
* THREE independent guards, because a login bypass reaching production is the
* worst bug this codebase could ship:
* THREE independent guards, unchanged by adding more accounts, because a login
* bypass reaching production is the worst bug this codebase could ship:
*
* 1. NODE_ENV must not be 'production'.
* 2. ALLOW_DEV_LOGIN must be explicitly 'true' — being in dev is not enough.
* 3. The phone number must match exactly.
* 3. The phone number must be one of the accounts listed below.
*
* Any one of them failing falls straight back to the real OTP path.
* Any one of them failing falls straight back to the real OTP path. That is why
* `/demo` says out loud that it only works off production: the honest fix for
* demoing against a live deployment is a real account and a real SMS, not a
* fourth flag that unlocks a bypass in production.
*/
const DEV_PHONE = '+34600000000';
const DEV_CODE = '000000';
export interface DemoAccount {
phone: string;
code: string;
/** Which side of the marketplace this account shows. */
role: 'client' | 'pro';
name: string;
/** What the person presenting should point at once they are in. */
blurb: string;
}
/**
* Both sides of the market.
*
* A one-sided demo of a two-sided marketplace shows half a product: the
* customer's deck is only interesting because a real tradesperson is on the
* other end of it. These map to seeded accounts (`pnpm db:seed`) that already
* own jobs, conversations and a completed booking, so the screens have something
* in them rather than five empty states.
*/
export const DEMO_ACCOUNTS: readonly DemoAccount[] = [
{
phone: '+34600000000',
code: DEV_CODE,
role: 'client',
name: 'Sofia Grau',
blurb:
'The customer. Has jobs in flight, a live conversation with a plumber, and one finished job waiting on a review.',
},
{
phone: '+34610000001',
code: DEV_CODE,
role: 'pro',
name: 'Marc Oliveras',
blurb:
'The tradesperson. Verified plumber — shows the card customers swipe, the verification state and the job side of chat.',
},
];
const DEMO_PHONES = DEMO_ACCOUNTS.map((a) => a.phone);
export function isDevLoginEnabled(): boolean {
return process.env.NODE_ENV !== 'production' && process.env.ALLOW_DEV_LOGIN === 'true';
}
export function isDevLoginPhone(phone: string): boolean {
return isDevLoginEnabled() && phone === DEV_PHONE;
return isDevLoginEnabled() && DEMO_PHONES.includes(phone);
}
/**
@@ -43,9 +86,11 @@ export async function pinDevLoginCode(phone: string): Promise<void> {
await db
.update(schema.verifications)
.set({ value: `${DEV_CODE}:0` })
.where(eq(schema.verifications.identifier, phone));
.where(inArray(schema.verifications.identifier, [phone]))
.catch(() => {});
console.info(`\n [dev login] ${DEV_PHONE} → code ${DEV_CODE}\n`);
console.info(`\n [dev login] ${phone} → code ${DEV_CODE}\n`);
}
export const DEV_LOGIN = { phone: DEV_PHONE, code: DEV_CODE } as const;
/** Kept for callers that only ever wanted the customer account. */
export const DEV_LOGIN = { phone: DEMO_ACCOUNTS[0]!.phone, code: DEV_CODE } as const;