{point.heading[locale]}
{point.text[locale]}
import { useEffect, type ReactNode } from 'react'; import { PLAN_IDS, PLANS, type VoiceAssignment } from '@wellformed/shared'; import { usePublicConfig } from '../api/hooks'; import { useI18n } from '../i18n'; import { useSession } from '../state/session'; import { useDismissSsr } from '../state/ssr'; import { useVoice } from '../state/voice'; import { workspacePath } from '../state/route'; /** * The page you land on with no account. * * Everything on it is either true of this install or a claim the demo can be * checked against — no logos, no numbers nobody counted, no testimonials. A * product whose pitch is "the SQL bar is honest" cannot open with a made-up * customer count. The limits quoted come from PLANS for the same reason: a * number typed in here would go stale the first time a plan moved. * * It is written for the person who *inherited* the spreadsheet, not the one who * built it — so the engine is described by what it refuses rather than by how it * works, and no sentence needs the words SQL, foreign key or expression to land. * The product is a spreadsheet and a relational engine at once, and the page has * to say both in that order: spreadsheet first, because that is the only thing * the visitor already knows how to use, and the rules in the same breath, never * later — refusing bad data is the whole product, and a visitor who was promised * only a spreadsheet reads their first rejection as a bug. * * Every word here is fixed except the four selling points in the middle, which * are drawn per visit from `shared/voice.ts` and recorded against how long that * visitor went on to actually use the product. The mechanism is in * `server/src/voice/`; what matters here is how little of the page it owns. * * That boundary was moved inward deliberately. An earlier version let the * headline and lede rotate too, and a page whose opening lines all had to * survive being read beside every other opening line could not be written as one * argument any more — each of them ended up saying everything and selling * nothing. Selling points do not have that problem: they are a list by nature, * four out of nine reads exactly like four out of nine, and which four is a real * question that taste cannot settle. * * What the page offers stays deliberately narrow: read the demo, or start a * workspace. */ export function MarketingScreen({ focus, ssrVoice, }: { focus?: 'pricing'; ssrVoice?: VoiceAssignment | null } = {}): ReactNode { const session = useSession(); const install = usePublicConfig(); const voice = useVoice(ssrVoice); const { t, locale } = useI18n(); useDismissSsr(); // The demo that speaks this page's language — not the browser's, which can // differ on /en and /nl. A page pinned to one language linking a demo in // another would undo the pinning one click in. const demos = install.data?.demos ?? []; const demo = demos.find((candidate) => candidate.locale === locale) ?? demos[0] ?? null; const signupsOpen = install.data?.signupsOpen ?? true; // `/pricing` is a real URL people paste at each other, so it has to arrive // at the prices rather than at the top of the page. useEffect(() => { if (focus !== 'pricing') return; const target = document.getElementById('pricing'); // Guarded: not every environment that renders this page can scroll one. target?.scrollIntoView?.({ block: 'start' }); }, [focus, install.data]); return (
{t('marketing.strapline')}
{t('marketing.lede')}
{signupsOpen ? t( 'marketing.freeToStart', t('marketing.limitTables', PLANS.free.tableLimit, locale), t('marketing.limitRows', PLANS.free.rowLimit, locale), ) : t('marketing.inviteOnly')}
{t('marketing.proofHint')}
{point.text[locale]}
{t('marketing.pricingHint')}
{blurb}
{t('marketing.vatHint')}
{t('marketing.closer')}
{signupsOpen && ( )}