diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 6f622bc..26e3440 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -2,6 +2,7 @@ name: Verify on: pull_request: push: + branches: [main] permissions: contents: read concurrency: @@ -19,6 +20,7 @@ jobs: cache: npm - run: npm ci - run: npm run lint + - run: npx oxfmt --check - run: npm run typecheck - run: npm test - run: npm run build diff --git a/README.md b/README.md index 303b511..1fba937 100644 --- a/README.md +++ b/README.md @@ -44,25 +44,25 @@ The homepage renders without Supabase. The inquiry form and the admin need it. | `OWNER_EMAIL` | script | setup only | Only `scripts/provision-owner.mjs` reads it. | | `E2E_OWNER_EMAIL`, `E2E_OWNER_PASSWORD`, `E2E_BASE_URL` | test | e2e only | Only Playwright reads them. The base URL defaults to `http://localhost:3000`. | -On Vercel, the API also accepts the origins in `VERCEL_URL` and `VERCEL_PROJECT_PRODUCTION_URL`, and reads the client IP from `x-vercel-forwarded-for`. Outside Vercel, every request counts against the same rate limit. +On Vercel, the API also accepts the origins in `VERCEL_URL`, `VERCEL_BRANCH_URL` and `VERCEL_PROJECT_PRODUCTION_URL`, and reads the client IP from `x-vercel-forwarded-for`. Outside Vercel, every request counts against the same rate limit. Give each environment its own database and secrets. Never add the `NEXT_PUBLIC_` prefix to a server secret. ## Commands -| Command | What it does | -| ------------------- | ----------------------------------------------------------------------------------------- | -| `npm run dev` | Starts the dev server. | -| `npm run lint` | Runs oxlint with type-aware rules. | -| `npm run format` | Formats with oxfmt. | -| `npm run typecheck` | Generates route types, then runs `tsc --noEmit`. | -| `npm test` | Runs validation and Slack tests, and runs the real migrations and RLS policies in PGlite. | -| `npm run test:e2e` | Runs Playwright against a running app and a real Supabase project. | -| `npm run test:public` | Runs public UI regressions against a production build without Supabase credentials. | -| `npm run test:session` | Checks login outage recovery and unauthorized denial against a local mock service. | -| `npm run build` | Builds for production. | - -CI (`.github/workflows/verify.yml`) runs lint, typecheck, tests, build, `npm audit` across production and development dependencies at every severity, public UI browser regressions, and local session-recovery checks on every push and pull request. CI does not run the authenticated provider e2e tests. +| Command | What it does | +| ---------------------- | ----------------------------------------------------------------------------------------- | +| `npm run dev` | Starts the dev server. | +| `npm run lint` | Runs oxlint with type-aware rules. | +| `npm run format` | Formats with oxfmt. | +| `npm run typecheck` | Generates route types, then runs `tsc --noEmit`. | +| `npm test` | Runs validation and Slack tests, and runs the real migrations and RLS policies in PGlite. | +| `npm run test:e2e` | Runs Playwright against a running app and a real Supabase project. | +| `npm run test:public` | Runs public UI regressions against a production build without Supabase credentials. | +| `npm run test:session` | Checks login outage recovery and unauthorized denial against a local mock service. | +| `npm run build` | Builds for production. | + +CI (`.github/workflows/verify.yml`) runs lint, a formatting check, typecheck, tests, build, `npm audit` across production and development dependencies at every severity, public UI browser regressions, and local session-recovery checks on every pull request and every push to `main`. CI does not run the authenticated provider e2e tests. To run public UI regressions locally, run `npm run build`, `npx playwright install chromium`, and `npm run test:public`. The tests start their own production server on port 3012. diff --git a/components/inquiry-form.tsx b/components/inquiry-form.tsx index ee74cb6..566e0fa 100644 --- a/components/inquiry-form.tsx +++ b/components/inquiry-form.tsx @@ -121,7 +121,13 @@ function ProjectRange({ ); } -export function InquiryForm({ caseStudy }: { caseStudy?: Study }) { +export function InquiryForm({ + caseStudy, + onSent, +}: { + caseStudy?: Study; + onSent?: () => void; +}) { const id = useId(); const submissionId = useRef(null); const [result, setResult] = useState({ kind: 'idle' }); @@ -171,6 +177,7 @@ export function InquiryForm({ caseStudy }: { caseStudy?: Study }) { ) { setResult({ kind: 'success', reference: body.reference }); submissionId.current = null; + onSent?.(); } else { const message = typeof body === 'object' && @@ -371,6 +378,9 @@ export function InquiryButton({ const dialog = useRef(null); const [portalTarget, setPortalTarget] = useState(null); const id = useId(); + // The dialog stays mounted to keep a draft; a sent request gets a fresh form next time. + const [sent, setSent] = useState(false); + const [formKey, setFormKey] = useState(0); useEffect(() => { if (!open) return; dialog.current?.showModal(); @@ -392,7 +402,13 @@ export function InquiryButton({ ref={dialog} className="inquiry-dialog" aria-labelledby={id} - onClose={() => setOpen(false)} + onClose={() => { + setOpen(false); + if (sent) { + setSent(false); + setFormKey((key) => key + 1); + } + }} >