// Projects — URL `/dashboard`. The live app screen AND the paywall showcase. // `useSubscription` streams the tenant's projects (first paint from the layout // gate, upgraded live on WS connect). Creating one calls `projects.create`, // which the api gates on the billing `projects` entitlement — the 4th on the // free plan rejects with a typed `EntitlementExceeded`, and we branch on that // (`isEntitlementExceeded`) to show the upgrade CTA instead of a raw error. import type { ReactNode } from 'react' import { useState, type FormEvent } from 'react' import type { PageMeta } from '@voltro/web' import { T, useT } from '@voltro/i18n' import { getCatalog } from '../../locales' import { isEntitlementExceeded, useMutation, useSubscription } from '../../lib/api' export const renderMode = 'ssr' as const export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({ title: getCatalog(locale)['meta.overview.title'], }) export default function Projects(): ReactNode { // Tag + row type both come from the generated procedure map — no annotation. const { data: live } = useSubscription('projects.list') const { mutate, pending } = useMutation('projects.create') const projects = live ?? [] const [name, setName] = useState('') const [paywalled, setPaywalled] = useState(false) const placeholder = useT('projects.new.placeholder') const onCreate = (e: FormEvent): void => { e.preventDefault() const trimmed = name.trim() if (trimmed === '') return void mutate({ name: trimmed }) .then(() => { setName(''); setPaywalled(false) }) .catch((err: unknown) => { if (isEntitlementExceeded(err)) setPaywalled(true) }) } return (

setName(e.target.value)} />
{paywalled ? (

) : null} {projects.length === 0 ? (

) : ( )}
) }