create-ailk 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-catalog.md +210 -52
- package/dist/cli.js +0 -0
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +14 -15
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +35 -4
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +471 -133
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +602 -29
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/layout.tsx +13 -1
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +20 -13
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -1
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +1 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/content/_site.mdx +12 -0
- package/templates/database/CHANGELOG.md +94 -0
- package/templates/database/inbox/schema.prisma +165 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1278
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #5048 — the reference mount.
|
|
3
|
-
*
|
|
4
|
-
* Two things are worth pinning here and nothing else is: that an emailed
|
|
5
|
-
* resume link's query parameters reach the section as a resume, and that a
|
|
6
|
-
* freshly-minted draft's id and token are written back into the URL rather
|
|
7
|
-
* than into a cookie or browser storage. Everything else about the surface is
|
|
8
|
-
* tested where it lives, in `packages/ui`.
|
|
9
|
-
*/
|
|
10
|
-
import { render, screen } from "@testing-library/react";
|
|
11
|
-
|
|
12
|
-
const replace = jest.fn();
|
|
13
|
-
let searchParams = new URLSearchParams();
|
|
14
|
-
|
|
15
|
-
jest.mock("next/navigation", () => ({
|
|
16
|
-
useRouter: () => ({ replace }),
|
|
17
|
-
useSearchParams: () => searchParams,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
// The section is the subject of its own tests; here it is a probe that records
|
|
21
|
-
// what the mount handed it.
|
|
22
|
-
const received: Record<string, unknown>[] = [];
|
|
23
|
-
jest.mock("@working-theory/ui", () => ({
|
|
24
|
-
ListingApplicationFlow: (props: Record<string, unknown>) => {
|
|
25
|
-
received.push(props);
|
|
26
|
-
return <div data-testid="listing-application-flow" />;
|
|
27
|
-
},
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
jest.mock("@working-theory/api-client", () => ({
|
|
31
|
-
projectListings: { start: jest.fn(), patchDraft: jest.fn(), submit: jest.fn() },
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
// The import sits below the mocks deliberately: `jest.mock` calls are hoisted,
|
|
35
|
-
// but the module under test must not be evaluated before they are registered.
|
|
36
|
-
import { ListingApplicationClient } from "../ListingApplicationClient";
|
|
37
|
-
|
|
38
|
-
const config = { slug: "apply", defaultLocale: "en", siteKey: "pk_test", steps: [] };
|
|
39
|
-
const messages = {} as never;
|
|
40
|
-
|
|
41
|
-
function mount() {
|
|
42
|
-
return render(
|
|
43
|
-
<ListingApplicationClient
|
|
44
|
-
config={config as never}
|
|
45
|
-
locale="en"
|
|
46
|
-
messages={messages}
|
|
47
|
-
/>,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
beforeEach(() => {
|
|
52
|
-
received.length = 0;
|
|
53
|
-
replace.mockClear();
|
|
54
|
-
searchParams = new URLSearchParams();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("ListingApplicationClient", () => {
|
|
58
|
-
it("passes no resume when the URL carries no link parameters", () => {
|
|
59
|
-
mount();
|
|
60
|
-
expect(screen.getByTestId("listing-application-flow")).toBeInTheDocument();
|
|
61
|
-
expect(received[0]).not.toHaveProperty("resume");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("rehydrates a draft from an emailed resume link's query parameters", () => {
|
|
65
|
-
searchParams = new URLSearchParams("draft=listing_7&token=tok_from_the_email");
|
|
66
|
-
mount();
|
|
67
|
-
expect(received[0]!["resume"]).toEqual({
|
|
68
|
-
id: "listing_7",
|
|
69
|
-
resumeToken: "tok_from_the_email",
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("needs BOTH parameters — a half link is not a resume", () => {
|
|
74
|
-
searchParams = new URLSearchParams("draft=listing_7");
|
|
75
|
-
mount();
|
|
76
|
-
expect(received[0]).not.toHaveProperty("resume");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("writes a newly minted draft back into the URL, not into a cookie", () => {
|
|
80
|
-
mount();
|
|
81
|
-
const onDraftStarted = received[0]!["onDraftStarted"] as (s: {
|
|
82
|
-
id: string;
|
|
83
|
-
resumeToken: string;
|
|
84
|
-
}) => void;
|
|
85
|
-
onDraftStarted({ id: "listing_9", resumeToken: "tok_new" });
|
|
86
|
-
|
|
87
|
-
expect(replace).toHaveBeenCalledWith(
|
|
88
|
-
"?draft=listing_9&token=tok_new",
|
|
89
|
-
{ scroll: false },
|
|
90
|
-
);
|
|
91
|
-
// The token is caller-held by design (#5048 (a)) — an httpOnly cookie
|
|
92
|
-
// cannot travel in an emailed link and a non-browser caller cannot present
|
|
93
|
-
// one at all. Nothing here may quietly set one.
|
|
94
|
-
expect(document.cookie).toBe("");
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* Tests for the scheduling module's booking route (#4633).
|
|
4
|
-
*
|
|
5
|
-
* Two things matter here and nothing else does. The page must hand the booker
|
|
6
|
-
* the event identifier and the module's own confirmation path, so a successful
|
|
7
|
-
* booking lands on this site rather than on the provider's; and it must never
|
|
8
|
-
* put a credential into the section data, because section data is exactly what
|
|
9
|
-
* crosses into the client island.
|
|
10
|
-
*/
|
|
11
|
-
import React from "react";
|
|
12
|
-
import { render } from "@testing-library/react";
|
|
13
|
-
|
|
14
|
-
jest.mock("next/navigation", () => ({
|
|
15
|
-
__esModule: true,
|
|
16
|
-
notFound: jest.fn(() => {
|
|
17
|
-
throw new Error("NEXT_NOT_FOUND");
|
|
18
|
-
}),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
jest.mock("next-intl/server", () => ({
|
|
22
|
-
__esModule: true,
|
|
23
|
-
// Echo the key back so an assertion names the message key, not the copy —
|
|
24
|
-
// the copy is the consuming site's to change.
|
|
25
|
-
getTranslations: jest.fn().mockResolvedValue((key: string) => key),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
// Capture what the page hands the catalog rather than rendering the real
|
|
29
|
-
// booker: the island's own behaviour is covered in packages/ui, and mounting it
|
|
30
|
-
// here would put a network stub between this test and the thing it asserts.
|
|
31
|
-
const capturedSections: unknown[] = [];
|
|
32
|
-
jest.mock("@working-theory/ui/sections", () => ({
|
|
33
|
-
__esModule: true,
|
|
34
|
-
SectionRenderer: ({ sections }: { sections: unknown[] }) => {
|
|
35
|
-
capturedSections.splice(0, capturedSections.length, ...sections);
|
|
36
|
-
return React.createElement("div", {
|
|
37
|
-
"data-testid": "section-renderer",
|
|
38
|
-
"data-section-count": String(sections.length),
|
|
39
|
-
});
|
|
40
|
-
},
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
import { notFound } from "next/navigation";
|
|
44
|
-
import SchedulePage from "../page";
|
|
45
|
-
|
|
46
|
-
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
47
|
-
|
|
48
|
-
const CAL_LINK = "acme/intro-call";
|
|
49
|
-
|
|
50
|
-
describe("SchedulePage — the module's booking route", () => {
|
|
51
|
-
const originalEnv = process.env["SCHEDULING_CAL_LINK"];
|
|
52
|
-
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
mockNotFound.mockClear();
|
|
55
|
-
capturedSections.length = 0;
|
|
56
|
-
process.env["SCHEDULING_CAL_LINK"] = CAL_LINK;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
afterAll(() => {
|
|
60
|
-
if (originalEnv === undefined) delete process.env["SCHEDULING_CAL_LINK"];
|
|
61
|
-
else process.env["SCHEDULING_CAL_LINK"] = originalEnv;
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("renders one booker section from the catalog", async () => {
|
|
65
|
-
const element = await SchedulePage({
|
|
66
|
-
params: Promise.resolve({ locale: "en" }),
|
|
67
|
-
});
|
|
68
|
-
const { container } = render(element);
|
|
69
|
-
|
|
70
|
-
expect(
|
|
71
|
-
container.querySelector('[data-testid="section-renderer"]'),
|
|
72
|
-
).not.toBeNull();
|
|
73
|
-
expect(capturedSections).toHaveLength(1);
|
|
74
|
-
expect((capturedSections[0] as { type: string }).type).toBe("booker");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("sends a successful booking to the page this module owns", async () => {
|
|
78
|
-
render(await SchedulePage({ params: Promise.resolve({ locale: "en" }) }));
|
|
79
|
-
expect(capturedSections).toHaveLength(1);
|
|
80
|
-
|
|
81
|
-
const data = (capturedSections[0] as { data: Record<string, unknown> }).data;
|
|
82
|
-
expect(data["calLink"]).toBe(CAL_LINK);
|
|
83
|
-
expect(data["bookedRedirect"]).toBe("/schedule/confirmed");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("puts no credential into the section data the client island receives", async () => {
|
|
87
|
-
render(await SchedulePage({ params: Promise.resolve({ locale: "en" }) }));
|
|
88
|
-
// Fixture self-guard: an empty capture would make every assertion below
|
|
89
|
-
// pass vacuously.
|
|
90
|
-
expect(capturedSections).toHaveLength(1);
|
|
91
|
-
|
|
92
|
-
// Whatever the page decides to pass, the SHAPE is fixed: the event
|
|
93
|
-
// identifier, page framing, and the redirect. Anything else appearing here
|
|
94
|
-
// is a value crossing into the browser that nobody decided to send.
|
|
95
|
-
const data = (capturedSections[0] as { data: Record<string, unknown> }).data;
|
|
96
|
-
expect(Object.keys(data).sort()).toEqual([
|
|
97
|
-
"bookedRedirect",
|
|
98
|
-
"calLink",
|
|
99
|
-
"eyebrow",
|
|
100
|
-
"headline",
|
|
101
|
-
]);
|
|
102
|
-
// Belt and braces: no serialized value may contain a credential-shaped
|
|
103
|
-
// string, however the page came by it.
|
|
104
|
-
const serialized = JSON.stringify(data);
|
|
105
|
-
expect(serialized).not.toMatch(/api[_-]?key/i);
|
|
106
|
-
expect(serialized).not.toMatch(/\bcal_(live|test)_/);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("404s when no event is configured", async () => {
|
|
110
|
-
delete process.env["SCHEDULING_CAL_LINK"];
|
|
111
|
-
|
|
112
|
-
await expect(
|
|
113
|
-
SchedulePage({ params: Promise.resolve({ locale: "en" }) }),
|
|
114
|
-
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
115
|
-
expect(mockNotFound).toHaveBeenCalled();
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* Tests for the scheduling module's confirmation route (#4633).
|
|
4
|
-
*
|
|
5
|
-
* The page's whole claim is that it is finished but owns no design: it renders
|
|
6
|
-
* real content from the site's own message catalog, through the site's own
|
|
7
|
-
* catalog components, and contributes no styling of its own. Both halves are
|
|
8
|
-
* asserted — that it renders a full page rather than a placeholder, and that
|
|
9
|
-
* every string in it came from the `schedule` namespace rather than from this
|
|
10
|
-
* file.
|
|
11
|
-
*/
|
|
12
|
-
import React from "react";
|
|
13
|
-
import { render } from "@testing-library/react";
|
|
14
|
-
|
|
15
|
-
const requestedKeys: string[] = [];
|
|
16
|
-
jest.mock("next-intl/server", () => ({
|
|
17
|
-
__esModule: true,
|
|
18
|
-
getTranslations: jest.fn().mockResolvedValue((key: string) => {
|
|
19
|
-
requestedKeys.push(key);
|
|
20
|
-
return `copy:${key}`;
|
|
21
|
-
}),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
const capturedSections: unknown[] = [];
|
|
25
|
-
jest.mock("@working-theory/ui/sections", () => ({
|
|
26
|
-
__esModule: true,
|
|
27
|
-
SectionRenderer: ({ sections }: { sections: unknown[] }) => {
|
|
28
|
-
capturedSections.splice(0, capturedSections.length, ...sections);
|
|
29
|
-
return React.createElement("div", {
|
|
30
|
-
"data-testid": "section-renderer",
|
|
31
|
-
"data-section-count": String(sections.length),
|
|
32
|
-
});
|
|
33
|
-
},
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
import ConfirmedPage, { generateMetadata } from "../page";
|
|
37
|
-
|
|
38
|
-
describe("ConfirmedPage — the module-owned confirmation page", () => {
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
capturedSections.length = 0;
|
|
41
|
-
requestedKeys.length = 0;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("renders a finished page, not a placeholder", async () => {
|
|
45
|
-
const element = await ConfirmedPage({
|
|
46
|
-
params: Promise.resolve({ locale: "en" }),
|
|
47
|
-
});
|
|
48
|
-
const { container } = render(element);
|
|
49
|
-
|
|
50
|
-
expect(
|
|
51
|
-
container.querySelector('[data-testid="section-renderer"]'),
|
|
52
|
-
).not.toBeNull();
|
|
53
|
-
// Confirmation, what happens next, and a way onward — three sections, all
|
|
54
|
-
// populated. A stub would be one section or none.
|
|
55
|
-
expect(capturedSections.map((s) => (s as { type: string }).type)).toEqual([
|
|
56
|
-
"hero",
|
|
57
|
-
"content-block",
|
|
58
|
-
"cta",
|
|
59
|
-
]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("takes every string from the site's own message catalog", async () => {
|
|
63
|
-
render(await ConfirmedPage({ params: Promise.resolve({ locale: "en" }) }));
|
|
64
|
-
expect(capturedSections).toHaveLength(3);
|
|
65
|
-
|
|
66
|
-
// Every rendered value resolved through getTranslations, so changing the
|
|
67
|
-
// copy is an edit to messages/<locale>.json — not a fork of this route.
|
|
68
|
-
const serialized = JSON.stringify(capturedSections);
|
|
69
|
-
for (const value of serialized.matchAll(/"(?:headline|subheadline|body|description|label)":"([^"]*)"/g)) {
|
|
70
|
-
expect(value[1]).toMatch(/^copy:confirmed\./);
|
|
71
|
-
}
|
|
72
|
-
expect(requestedKeys.length).toBeGreaterThan(0);
|
|
73
|
-
expect(requestedKeys.every((k) => k.startsWith("confirmed."))).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("declares no styling of its own — the site's design system decides", async () => {
|
|
77
|
-
render(await ConfirmedPage({ params: Promise.resolve({ locale: "en" }) }));
|
|
78
|
-
// Fixture self-guard: an empty capture would make the assertion vacuous.
|
|
79
|
-
expect(capturedSections).toHaveLength(3);
|
|
80
|
-
|
|
81
|
-
// No className, no color, no spacing anywhere in what the page contributes.
|
|
82
|
-
// This is what lets a site with its own design system adopt the page
|
|
83
|
-
// without fighting it.
|
|
84
|
-
const serialized = JSON.stringify(capturedSections);
|
|
85
|
-
expect(serialized).not.toMatch(/className|style|#[0-9a-fA-F]{3,8}\b/);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("is not indexable", async () => {
|
|
89
|
-
// Reachable only by having just booked; indexing it would surface a
|
|
90
|
-
// confirmation page to people who never made a booking.
|
|
91
|
-
const metadata = await generateMetadata({
|
|
92
|
-
params: Promise.resolve({ locale: "en" }),
|
|
93
|
-
});
|
|
94
|
-
expect(metadata.robots).toEqual({ index: false, follow: true });
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* /[locale]/schedule/confirmed — the scheduling module's confirmation page
|
|
4
|
-
* (#4633). The destination `BookerIsland` navigates to the moment the booking
|
|
5
|
-
* POST resolves (its `bookedRedirect` prop), so the visitor never leaves this
|
|
6
|
-
* site to find out the booking worked.
|
|
7
|
-
*
|
|
8
|
-
* The module OWNS this page. It is not a marketing page that happens to follow
|
|
9
|
-
* a booker: it ships with the module, it is gated with the module, and a
|
|
10
|
-
* scaffold without the module has no such route. That is why it lives under
|
|
11
|
-
* `schedule/` rather than at `/thanks/<slug>` — `/thanks` is a core marketing
|
|
12
|
-
* surface every scaffold carries, and a page in it would outlive the module
|
|
13
|
-
* that gave it a reason to exist.
|
|
14
|
-
*
|
|
15
|
-
* FINISHED, NOT A STUB — and it owns no design of its own. Every visual
|
|
16
|
-
* decision resolves through the consuming site's theme tokens and its
|
|
17
|
-
* `SectionRenderer` catalog; this file contributes a section list and nothing
|
|
18
|
-
* else. The copy is read from the site's own `schedule` message namespace,
|
|
19
|
-
* so customizing the page is an edit to `messages/<locale>.json` — the same
|
|
20
|
-
* surface the site already edits for every other page — not a fork of this
|
|
21
|
-
* route. A site whose design system differs has nothing here to fight.
|
|
22
|
-
*
|
|
23
|
-
* Carries no booking detail because it receives none: the redirect is a
|
|
24
|
-
* client-side `router.push` with no state, and re-reading the booking would
|
|
25
|
-
* mean a second provider call to tell the visitor something the confirmation
|
|
26
|
-
* email already carries. It confirms, and it says what happens next.
|
|
27
|
-
*
|
|
28
|
-
* `noindex`: a booking confirmation is reachable only by having just booked.
|
|
29
|
-
*/
|
|
30
|
-
import { SectionRenderer } from "@working-theory/ui/sections";
|
|
31
|
-
import type { PageSection } from "@working-theory/validation";
|
|
32
|
-
import type { Metadata } from "next";
|
|
33
|
-
import { getTranslations } from "next-intl/server";
|
|
34
|
-
import type { ReactElement } from "react";
|
|
35
|
-
|
|
36
|
-
interface Props {
|
|
37
|
-
params: Promise<{ locale: string }>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
41
|
-
const { locale } = await params;
|
|
42
|
-
const t = await getTranslations({ locale, namespace: "schedule" });
|
|
43
|
-
return {
|
|
44
|
-
title: t("confirmed.title"),
|
|
45
|
-
description: t("confirmed.description"),
|
|
46
|
-
robots: { index: false, follow: true },
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export default async function Page({ params }: Props): Promise<ReactElement> {
|
|
51
|
-
const { locale } = await params;
|
|
52
|
-
const t = await getTranslations({ locale, namespace: "schedule" });
|
|
53
|
-
|
|
54
|
-
const sections: PageSection[] = [
|
|
55
|
-
{
|
|
56
|
-
id: "schedule-confirmed-hero",
|
|
57
|
-
type: "hero",
|
|
58
|
-
data: {
|
|
59
|
-
headline: t("confirmed.headline"),
|
|
60
|
-
subheadline: t("confirmed.subheadline"),
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: "schedule-confirmed-prepare",
|
|
65
|
-
type: "content-block",
|
|
66
|
-
data: {
|
|
67
|
-
headline: t("confirmed.prepareHeadline"),
|
|
68
|
-
body: t("confirmed.prepareBody"),
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: "schedule-confirmed-cta",
|
|
73
|
-
type: "cta",
|
|
74
|
-
data: {
|
|
75
|
-
headline: t("confirmed.ctaHeadline"),
|
|
76
|
-
description: t("confirmed.ctaDescription"),
|
|
77
|
-
primaryCta: { label: t("confirmed.ctaPrimaryLabel"), href: "/" },
|
|
78
|
-
secondaryCta: {
|
|
79
|
-
label: t("confirmed.ctaSecondaryLabel"),
|
|
80
|
-
href: "/contact",
|
|
81
|
-
variant: "ghost",
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<main>
|
|
89
|
-
<SectionRenderer sections={sections} locale={locale} />
|
|
90
|
-
</main>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* /[locale]/schedule — the scheduling module's booking page (#4633).
|
|
4
|
-
*
|
|
5
|
-
* Exists only because the `scheduling` module is installed: `create-ailk
|
|
6
|
-
* --modules marketing,scheduling` copies this directory, and a scaffold
|
|
7
|
-
* without the module never receives it (packages/create-ailk/src/surfaces.ts,
|
|
8
|
-
* the `schedule` gateable surface).
|
|
9
|
-
*
|
|
10
|
-
* API-backed rather than MDX-backed, the same shape as the `flows` surface:
|
|
11
|
-
* everything the page shows about the event — its title, its duration, its
|
|
12
|
-
* meeting type, its open slots — is DERIVED by the `booker` section from
|
|
13
|
-
* apps/api's `/v1/schedule/availability`, so there is no content file that
|
|
14
|
-
* could disagree with the calendar. The one thing the scaffolder cannot know
|
|
15
|
-
* is WHICH event to book, so that arrives as configuration.
|
|
16
|
-
*
|
|
17
|
-
* CREDENTIALS. `SCHEDULING_CAL_LINK` is read here, on the server, and it is
|
|
18
|
-
* not the API key — the key lives in apps/api's scheduling service and is
|
|
19
|
-
* never read by this app at all. `calLink` is a public identifier (it is the
|
|
20
|
-
* provider's own public booking-page path) and reaching the browser is its
|
|
21
|
-
* job: the booker sends it back to `/v1/schedule/*` to name the event. It is
|
|
22
|
-
* deliberately NOT a `NEXT_PUBLIC_` variable so that the whole scheduling
|
|
23
|
-
* config surface stays server-read, which is the rule architecture.yaml's
|
|
24
|
-
* scheduling block enforces.
|
|
25
|
-
*
|
|
26
|
-
* Unconfigured is a 404, not an error page — the flows precedent: to a
|
|
27
|
-
* visitor, a booking page for an event the operator never named is
|
|
28
|
-
* indistinguishable from a page that does not exist.
|
|
29
|
-
*/
|
|
30
|
-
import { SectionRenderer } from "@working-theory/ui/sections";
|
|
31
|
-
import { registerSchedulingSections } from "@working-theory/ui/slices/scheduling";
|
|
32
|
-
import type { PageSection } from "@working-theory/validation";
|
|
33
|
-
import type { Metadata } from "next";
|
|
34
|
-
import { notFound } from "next/navigation";
|
|
35
|
-
import { getTranslations } from "next-intl/server";
|
|
36
|
-
import type { ReactElement } from "react";
|
|
37
|
-
|
|
38
|
-
// `SectionRenderer`'s oss dispatch maps `booker` to a placeholder, because the
|
|
39
|
-
// real component strips with this slice; this call contributes the real one and
|
|
40
|
-
// must stay an explicit statement, not a bare side-effect import (#5214 D4).
|
|
41
|
-
registerSchedulingSections();
|
|
42
|
-
|
|
43
|
-
/** Site-relative path of the confirmation page this module also owns. */
|
|
44
|
-
const BOOKED_REDIRECT = "/schedule/confirmed";
|
|
45
|
-
|
|
46
|
-
interface Props {
|
|
47
|
-
params: Promise<{ locale: string }>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
51
|
-
const { locale } = await params;
|
|
52
|
-
const t = await getTranslations({ locale, namespace: "schedule" });
|
|
53
|
-
return { title: t("title"), description: t("description") };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export default async function Page({ params }: Props): Promise<ReactElement> {
|
|
57
|
-
const { locale } = await params;
|
|
58
|
-
const calLink = process.env["SCHEDULING_CAL_LINK"];
|
|
59
|
-
if (!calLink) {
|
|
60
|
-
notFound();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const t = await getTranslations({ locale, namespace: "schedule" });
|
|
64
|
-
const sections: PageSection[] = [
|
|
65
|
-
{
|
|
66
|
-
id: "schedule-booker",
|
|
67
|
-
type: "booker",
|
|
68
|
-
data: {
|
|
69
|
-
calLink,
|
|
70
|
-
eyebrow: t("eyebrow"),
|
|
71
|
-
headline: t("headline"),
|
|
72
|
-
bookedRedirect: BOOKED_REDIRECT,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<main>
|
|
79
|
-
<SectionRenderer sections={sections} locale={locale} />
|
|
80
|
-
</main>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
Blog posts live here — one .mdx file per post.
|
|
2
|
-
|
|
3
|
-
The /blog route is OFF by default. Turn it on by scaffolding the blog surface:
|
|
4
|
-
|
|
5
|
-
npx create-ailk --sync-architecture marketing --surfaces blog .
|
|
6
|
-
|
|
7
|
-
then add the route with the `scaffold-page` skill, or re-scaffold with
|
|
8
|
-
`--surfaces blog`.
|
|
9
|
-
|
|
10
|
-
Frontmatter contract (enforced at build time by source.config.ts):
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
title: string # required
|
|
14
|
-
description: string # required
|
|
15
|
-
date: 2026-01-31 # optional, ISO date — datePublished
|
|
16
|
-
updated: 2026-02-14 # optional, ISO date — dateModified
|
|
17
|
-
image: /og/my-post.png # optional
|
|
18
|
-
author: # optional
|
|
19
|
-
name: Jane Doe
|
|
20
|
-
avatar: /avatars/jane.png
|
|
21
|
-
tags: [launch, notes] # optional
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
This file is a placeholder so the directory survives packaging. Delete it once
|
|
25
|
-
you have posts — it is not read by anything (the loader globs *.mdx only).
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Documentation
|
|
3
|
-
description: Documentation for {{COMPANY_NAME}}. Replace this page with your own getting-started guide.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Documentation
|
|
7
|
-
|
|
8
|
-
This is the docs landing page. Replace it with whatever a new reader of your
|
|
9
|
-
documentation needs first.
|
|
10
|
-
|
|
11
|
-
## Adding a page
|
|
12
|
-
|
|
13
|
-
Every `.mdx` file in this directory becomes a page under `/docs`. The filename
|
|
14
|
-
is the URL segment; `index.mdx` is the section root. Two frontmatter fields are
|
|
15
|
-
required:
|
|
16
|
-
|
|
17
|
-
```yaml
|
|
18
|
-
---
|
|
19
|
-
title: string
|
|
20
|
-
description: string
|
|
21
|
-
---
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Turning docs off
|
|
25
|
-
|
|
26
|
-
The `/docs` route is part of the `docs` surface. If your site does not need
|
|
27
|
-
documentation, re-scaffold without it and the route will not exist — an absent
|
|
28
|
-
route is better than one serving an empty page.
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Jest stub for @/.source/server — the fumadocs-mdx codegen output.
|
|
3
|
-
*
|
|
4
|
-
* The real .source/server.ts uses top-level await and ESM-only imports from
|
|
5
|
-
* fumadocs-mdx/runtime/server, which ts-jest cannot load in CJS mode.
|
|
6
|
-
* This stub provides the minimal shape needed by lib/source.ts so that
|
|
7
|
-
* the loader() call in that file does not throw at import time.
|
|
8
|
-
*
|
|
9
|
-
* Tests that exercise the blog/docs/llms routes inject their own source
|
|
10
|
-
* stubs via props, so the loader's output is never used in test execution.
|
|
11
|
-
*
|
|
12
|
-
* Referenced by: apps/web/jest.config.cjs moduleNameMapper '^@/\\.source/server$'
|
|
13
|
-
* Added: issue #1077 — fumadocs-core ESM + .source/server CJS compatibility
|
|
14
|
-
* Updated: issue #1459 — added docs collection stub alongside blog
|
|
15
|
-
*
|
|
16
|
-
* @note The real .source/ directory is gitignored (codegen output).
|
|
17
|
-
* This committed stub lives in lib/__mocks__/ so it is tracked by git.
|
|
18
|
-
*/
|
|
19
|
-
"use strict";
|
|
20
|
-
|
|
21
|
-
exports.blog = {
|
|
22
|
-
toFumadocsSource: function () {
|
|
23
|
-
return { files: [] };
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
exports.docs = {
|
|
28
|
-
toFumadocsSource: function () {
|
|
29
|
-
return { files: [] };
|
|
30
|
-
},
|
|
31
|
-
};
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter: Fumadocs blog source → `BlogListDataSchema` (issue #2713).
|
|
3
|
-
*
|
|
4
|
-
* The `@working-theory/ui` `BlogList` section is presentational (ADR-0027): it renders,
|
|
5
|
-
* the app maps source → data. This adapter turns `blogSource.getPages()` into
|
|
6
|
-
* the section's `data` prop, applying the field mapping from the issue:
|
|
7
|
-
*
|
|
8
|
-
* title ← frontmatter.title
|
|
9
|
-
* slug ← page.url (with the `/blog/` baseUrl prefix stripped, since
|
|
10
|
-
* the section re-prepends it: `href={`/blog/${slug}`}`)
|
|
11
|
-
* excerpt ← frontmatter.description
|
|
12
|
-
* publishedAt ← frontmatter.date
|
|
13
|
-
* coverImage ← frontmatter.image
|
|
14
|
-
* author ← frontmatter.author
|
|
15
|
-
* categories ← frontmatter.tags
|
|
16
|
-
*
|
|
17
|
-
* Locale-aware: the locale is threaded so callers can pass a locale-prefixed
|
|
18
|
-
* baseUrl for pagination when the route paginates (>1 page).
|
|
19
|
-
*/
|
|
20
|
-
import type { BlogListData } from "@working-theory/validation";
|
|
21
|
-
|
|
22
|
-
/** Frontmatter shape consumed here — must match `source.config.ts` blog schema. */
|
|
23
|
-
interface BlogFrontmatter {
|
|
24
|
-
title: string;
|
|
25
|
-
description: string;
|
|
26
|
-
date?: string;
|
|
27
|
-
updated?: string;
|
|
28
|
-
image?: string;
|
|
29
|
-
author?: { name: string; avatar?: string };
|
|
30
|
-
tags?: string[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** The subset of a Fumadocs page entry this adapter reads. */
|
|
34
|
-
interface BlogPageEntry {
|
|
35
|
-
url: string;
|
|
36
|
-
data: unknown;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Optional pagination inputs; pagination is emitted only when totalPages > 1. */
|
|
40
|
-
interface BlogListPagination {
|
|
41
|
-
page: number;
|
|
42
|
-
totalPages: number;
|
|
43
|
-
baseUrl: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type BlogPostPreview = BlogListData["posts"][number];
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Strip the `/blog/` baseUrl prefix the section re-prepends when linking
|
|
50
|
-
* (`href={`/blog/${slug}`}`). The bare baseUrl (`/blog`, the index page that
|
|
51
|
-
* `getPages()` also returns) maps to an empty slug so the link stays `/blog/`
|
|
52
|
-
* rather than doubling the prefix.
|
|
53
|
-
*/
|
|
54
|
-
function toSlug(url: string): string {
|
|
55
|
-
if (url === "/blog") return "";
|
|
56
|
-
return url.replace(/^\/blog\//, "");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function toPreview(entry: BlogPageEntry): BlogPostPreview {
|
|
60
|
-
const fm = entry.data as BlogFrontmatter;
|
|
61
|
-
return {
|
|
62
|
-
title: fm.title,
|
|
63
|
-
slug: toSlug(entry.url),
|
|
64
|
-
excerpt: fm.description,
|
|
65
|
-
publishedAt: fm.date ?? "",
|
|
66
|
-
...(fm.image ? { coverImage: { src: fm.image, alt: fm.title } } : {}),
|
|
67
|
-
...(fm.author
|
|
68
|
-
? {
|
|
69
|
-
author: {
|
|
70
|
-
name: fm.author.name,
|
|
71
|
-
...(fm.author.avatar
|
|
72
|
-
? { avatar: { src: fm.author.avatar, alt: fm.author.name } }
|
|
73
|
-
: {}),
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
: {}),
|
|
77
|
-
...(fm.tags && fm.tags.length > 0 ? { categories: fm.tags } : {}),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Map Fumadocs blog pages to `BlogList` section data.
|
|
83
|
-
*
|
|
84
|
-
* @param pages blog source entries (`blogSource.getPages()`).
|
|
85
|
-
* @param pagination optional pagination; emitted only when `totalPages > 1`.
|
|
86
|
-
*/
|
|
87
|
-
export function toBlogListData(
|
|
88
|
-
pages: BlogPageEntry[],
|
|
89
|
-
pagination?: BlogListPagination,
|
|
90
|
-
): BlogListData {
|
|
91
|
-
return {
|
|
92
|
-
posts: pages.map(toPreview),
|
|
93
|
-
...(pagination && pagination.totalPages > 1 ? { pagination } : {}),
|
|
94
|
-
};
|
|
95
|
-
}
|