create-ailk 0.2.0 → 0.4.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.
Files changed (292) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/cli.js +19 -9
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +51 -3
  6. package/dist/lib/extract-module-bundle.js +327 -28
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/fetch-template.d.ts +20 -0
  10. package/dist/lib/fetch-template.js +129 -0
  11. package/dist/lib/module-license-gate.d.ts +10 -0
  12. package/dist/lib/module-license-gate.js +13 -4
  13. package/dist/lib/paid-paths.d.ts +16 -0
  14. package/dist/lib/paid-paths.js +90 -0
  15. package/dist/module-architecture.d.ts +197 -23
  16. package/dist/module-architecture.js +820 -25
  17. package/dist/parse-args.d.ts +9 -2
  18. package/dist/parse-args.js +46 -1
  19. package/dist/programmatic.d.ts +28 -2
  20. package/dist/programmatic.js +115 -8
  21. package/dist/surfaces.d.ts +12 -36
  22. package/dist/surfaces.js +77 -5
  23. package/dist/sync-routes.js +31 -1
  24. package/dist/template-manifest.d.ts +92 -0
  25. package/dist/template-manifest.js +168 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +44 -0
  28. package/package.json +3 -2
  29. package/site-templates/marketing-site-free/ailk-template.json +8 -0
  30. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
  31. package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site-free/provenance.json +4 -0
  34. package/templates/.claude/agents/web.md +2 -3
  35. package/templates/.claude/rules/architecture.md +5 -5
  36. package/templates/.claude/skills/README.md +2 -1
  37. package/templates/.env.example +21 -9
  38. package/templates/CONVENTIONS.md +6 -7
  39. package/templates/apps/api/.env.example +9 -0
  40. package/templates/apps/api/CLAUDE.md +33 -5
  41. package/templates/apps/api/package.json +0 -1
  42. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  43. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  44. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  45. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
  46. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  47. package/templates/apps/api/src/config/index.ts +19 -0
  48. package/templates/apps/api/src/config/modules.ts +12 -14
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  50. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  51. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  52. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  53. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  54. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  55. package/templates/apps/api/src/openapi/index.ts +9 -3
  56. package/templates/apps/api/src/openapi/spec.ts +505 -65
  57. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  58. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  59. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  60. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  61. package/templates/apps/api/src/routes/content/index.ts +38 -11
  62. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  63. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  64. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  65. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  66. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  67. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  68. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  69. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  70. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  71. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  72. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  73. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  74. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  75. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  76. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  77. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  78. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  79. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  80. package/templates/apps/api/src/server.ts +504 -137
  81. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  82. package/templates/apps/api/src/services/listing-config.ts +58 -0
  83. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  84. package/templates/apps/api/src/services/project-listings.ts +229 -4
  85. package/templates/apps/api/src/vercel-handler.ts +60 -26
  86. package/templates/apps/mcp/.env.example +8 -0
  87. package/templates/apps/mcp/CLAUDE.md +2 -2
  88. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  89. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  90. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  91. package/templates/apps/mcp/src/config/modules.ts +14 -13
  92. package/templates/apps/mcp/src/server.ts +8 -4
  93. package/templates/apps/mcp/src/tools/index.ts +7 -22
  94. package/templates/apps/web/.env.example +15 -0
  95. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  96. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  97. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  98. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  102. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  104. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  105. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  106. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  107. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  108. package/templates/apps/web/jest.config.cjs +19 -13
  109. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  110. package/templates/apps/web/lib/server-api.ts +28 -12
  111. package/templates/apps/web/messages/en.json +58 -0
  112. package/templates/apps/web/next.config.mjs +1 -3
  113. package/templates/apps/web/package.json +0 -6
  114. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  115. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  116. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  117. package/templates/apps/web/public/favicon-16x16.png +0 -0
  118. package/templates/apps/web/public/favicon-32x32.png +0 -0
  119. package/templates/apps/web/public/favicon.ico +0 -0
  120. package/templates/apps/web/public/favicon.svg +6 -3
  121. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  122. package/templates/apps/web/public/logomark.svg +4 -0
  123. package/templates/apps/web/public/site.webmanifest +2 -2
  124. package/templates/apps/web/public/wordmark.svg +4 -0
  125. package/templates/database/CHANGELOG.md +47 -0
  126. package/templates/database/inbox/schema.prisma +67 -0
  127. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  128. package/templates/database/package.json +1 -1
  129. package/templates/package.json +1 -1
  130. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  131. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  132. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  133. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  134. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  135. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  136. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  137. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  138. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  139. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  140. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  141. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  142. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  143. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  144. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  145. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  146. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  147. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  148. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  149. package/templates/apps/api/src/lib/stripe.ts +0 -52
  150. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  151. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  152. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  153. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  154. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  155. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  156. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  157. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  158. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  159. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  160. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  161. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  162. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  163. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  164. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  165. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  166. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  167. package/templates/apps/api/src/routes/flows/README.md +0 -147
  168. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  169. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  170. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  171. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  172. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  173. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  174. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  175. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  176. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  177. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  178. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  179. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  180. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  181. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  182. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  183. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  184. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  185. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  186. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  187. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  188. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  189. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  190. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  191. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  192. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  193. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  194. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  195. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  196. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  197. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  198. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  199. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  200. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  201. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  202. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  203. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  204. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  205. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  206. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  207. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  208. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  209. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  210. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  211. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  212. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  213. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  214. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  215. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  216. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  217. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  218. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  219. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  220. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  221. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  222. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  223. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  224. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  225. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  226. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  227. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  228. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  229. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  230. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  231. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  232. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  233. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  234. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  235. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  236. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  237. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  238. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  239. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  240. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  241. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  242. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  243. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  244. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  245. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  246. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  247. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  248. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  249. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  250. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  258. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  259. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  260. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  261. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  266. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  267. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  268. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  269. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  270. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  271. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  272. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  273. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  274. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  275. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  276. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  277. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  278. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  279. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  280. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  281. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  282. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  283. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  284. package/templates/apps/web/content/en/blog/README.txt +0 -25
  285. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  286. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  287. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  288. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  289. package/templates/apps/web/lib/source.ts +0 -34
  290. package/templates/apps/web/public/logomark-dark.svg +0 -4
  291. package/templates/apps/web/public/logomark-light.svg +0 -4
  292. package/templates/apps/web/source.config.ts +0 -78
@@ -1,396 +0,0 @@
1
- /**
2
- * Tests for the `/[locale]/flows/[slug]/results/[payload]` cold-load route
3
- * (#4770).
4
- *
5
- * AC3 — a payload encoding a known answer set renders exactly the offer
6
- * names and matched reasons `recommend()` returns for those same
7
- * answers — asserted against the SAME fixture (`FIXTURE_CONFIG` +
8
- * `KNOWN_ANSWERS`) `recommend()` itself would score.
9
- * AC4 — four cases `redirect()` to `/[locale]/flows/[slug]` and render no
10
- * results markup: invalid payload, stale-version payload, a
11
- * flowSlug-mismatched payload, and a non-recommending flow.
12
- *
13
- * `next/navigation`'s `redirect` is mocked to throw (mirrors the sibling
14
- * `thanks/[slug]` route's `notFound` test pattern) so a redirect is
15
- * observable via `.rejects.toThrow` without a real Next.js router. `next-intl`
16
- * is mocked to an identity translator — `ResultsClient`'s only string comes
17
- * from the `flow` namespace's `whyThis` key, which this test does not assert
18
- * on by literal text.
19
- */
20
- import { render, screen } from "@testing-library/react";
21
- import userEvent from "@testing-library/user-event";
22
- import type { FlowConfig } from "@working-theory/validation";
23
- import {
24
- encodePermalink,
25
- PERMALINK_VERSION,
26
- recommend,
27
- resolveLocalizedString,
28
- } from "@working-theory/validation";
29
-
30
- const mockGet = jest.fn();
31
- jest.mock("@working-theory/api-client", () => ({
32
- __esModule: true,
33
- flows: { get: (...args: unknown[]) => mockGet(...args) },
34
- }));
35
-
36
- const mockRedirect = jest.fn((url: string) => {
37
- throw new Error(`NEXT_REDIRECT:${url}`);
38
- });
39
- jest.mock("next/navigation", () => ({
40
- __esModule: true,
41
- redirect: (url: string) => mockRedirect(url),
42
- }));
43
-
44
- jest.mock("next-intl", () => ({
45
- __esModule: true,
46
- useTranslations: () => (key: string) => key,
47
- useMessages: () => ({}),
48
- }));
49
-
50
- import Page, { generateMetadata } from "../page";
51
-
52
- const SLUG = "ailk-recommender";
53
-
54
- const KNOWN_ANSWERS = { team_size: "solo", wants_seo: "yes" };
55
-
56
- function fixtureConfig(overrides: Partial<FlowConfig> = {}): FlowConfig {
57
- return {
58
- slug: SLUG,
59
- defaultLocale: "en",
60
- mode: "recommending",
61
- steps: [
62
- {
63
- key: "team",
64
- type: "segmentation",
65
- copy: { title: { en: "Tell us about your team" } },
66
- questions: [
67
- {
68
- name: "team_size",
69
- options: [
70
- { value: "solo", label: { en: "Solo" } },
71
- { value: "agency", label: { en: "Agency" } },
72
- ],
73
- },
74
- {
75
- name: "wants_seo",
76
- options: [
77
- { value: "yes", label: { en: "Yes" } },
78
- { value: "no", label: { en: "No" } },
79
- ],
80
- },
81
- ],
82
- },
83
- ],
84
- offers: [
85
- {
86
- key: "free_oss",
87
- copy: {
88
- name: { en: "Free" },
89
- description: { en: "Self-serve, open source." },
90
- },
91
- price: { kind: "label", label: { en: "Free" } },
92
- rules: [
93
- {
94
- when: { answer: "team_size", eq: "solo" },
95
- reason: { en: "You're a solo builder." },
96
- },
97
- ],
98
- },
99
- {
100
- key: "seo_addon",
101
- copy: {
102
- name: { en: "SEO Add-on" },
103
- description: { en: "AEO + SEO tuning." },
104
- },
105
- price: { kind: "label", label: { en: "Add-on" } },
106
- rules: [
107
- {
108
- when: { answer: "wants_seo", eq: "yes" },
109
- reason: { en: "You want more organic reach." },
110
- },
111
- ],
112
- },
113
- ],
114
- recommendation: {
115
- key: "recommendation",
116
- copy: { title: { en: "Your recommendation" } },
117
- },
118
- ...overrides,
119
- } as FlowConfig;
120
- }
121
-
122
- beforeEach(() => {
123
- mockGet.mockReset();
124
- mockRedirect.mockClear();
125
- });
126
-
127
- async function expectRedirectTo(
128
- payload: string,
129
- expectedUrl: string,
130
- ): Promise<void> {
131
- await expect(
132
- Page({
133
- params: Promise.resolve({ locale: "en", slug: SLUG, payload }),
134
- }),
135
- ).rejects.toThrow(`NEXT_REDIRECT:${expectedUrl}`);
136
- expect(mockRedirect).toHaveBeenCalledWith(expectedUrl);
137
- }
138
-
139
- describe("AC4 — invalid / stale-version / mismatched / non-recommending all redirect to step 1", () => {
140
- it("redirects on an invalid payload", async () => {
141
- await expectRedirectTo("not-a-valid-payload!!!", `/en/flows/${SLUG}`);
142
- // Never fetched the flow config for an undecodable payload.
143
- expect(mockGet).not.toHaveBeenCalled();
144
- });
145
-
146
- it("redirects on a stale-version payload", async () => {
147
- const stalePayload = encodePermalink({
148
- version: PERMALINK_VERSION - 1,
149
- flowSlug: SLUG,
150
- answers: KNOWN_ANSWERS,
151
- });
152
- await expectRedirectTo(stalePayload, `/en/flows/${SLUG}`);
153
- expect(mockGet).not.toHaveBeenCalled();
154
- });
155
-
156
- it("redirects when the payload's own flowSlug differs from the route's [slug]", async () => {
157
- const mismatchedPayload = encodePermalink({
158
- version: PERMALINK_VERSION,
159
- flowSlug: "a-different-flow",
160
- answers: KNOWN_ANSWERS,
161
- });
162
- await expectRedirectTo(mismatchedPayload, `/en/flows/${SLUG}`);
163
- expect(mockGet).not.toHaveBeenCalled();
164
- });
165
-
166
- it("redirects when the served flow is not a recommending flow (no offers)", async () => {
167
- const payload = encodePermalink({
168
- version: PERMALINK_VERSION,
169
- flowSlug: SLUG,
170
- answers: KNOWN_ANSWERS,
171
- });
172
- const {
173
- offers: _offers,
174
- mode: _mode,
175
- ...nonRecommending
176
- } = fixtureConfig();
177
- mockGet.mockResolvedValueOnce({ ok: true, body: nonRecommending });
178
-
179
- await expectRedirectTo(payload, `/en/flows/${SLUG}`);
180
- });
181
-
182
- it("redirects when the served flow declares mode !== 'recommending'", async () => {
183
- const payload = encodePermalink({
184
- version: PERMALINK_VERSION,
185
- flowSlug: SLUG,
186
- answers: KNOWN_ANSWERS,
187
- });
188
- mockGet.mockResolvedValueOnce({
189
- ok: true,
190
- body: fixtureConfig({ mode: "qualifying" }),
191
- });
192
-
193
- await expectRedirectTo(payload, `/en/flows/${SLUG}`);
194
- });
195
-
196
- it("no results markup renders on any redirect path (renders nothing before throwing)", async () => {
197
- await expect(
198
- Page({
199
- params: Promise.resolve({
200
- locale: "en",
201
- slug: SLUG,
202
- payload: "garbage",
203
- }),
204
- }),
205
- ).rejects.toThrow();
206
- expect(screen.queryByTestId("flow-results")).not.toBeInTheDocument();
207
- });
208
- });
209
-
210
- describe("AC3 — a valid payload against a recommending flow renders exactly recommend()'s fits", () => {
211
- it("renders exactly the offer names and matched reasons recommend() itself returns for the same answers", async () => {
212
- const user = userEvent.setup();
213
- const config = fixtureConfig();
214
- const payload = encodePermalink({
215
- version: PERMALINK_VERSION,
216
- flowSlug: SLUG,
217
- answers: KNOWN_ANSWERS,
218
- });
219
- mockGet.mockResolvedValueOnce({ ok: true, body: config });
220
-
221
- // The SAME fixture (`config` + `KNOWN_ANSWERS`) drives BOTH the mocked
222
- // server response AND this expectation — `recommend()` is the one
223
- // source of truth for "which fits, in what order, with which reason,"
224
- // never a second hand-typed list that could silently drift from it.
225
- const expected = recommend(
226
- {
227
- key: SLUG,
228
- offers: config.offers,
229
- pairings: config.pairings,
230
- rateTable: config.rateTable,
231
- scoring: config.scoring,
232
- },
233
- KNOWN_ANSWERS,
234
- );
235
- expect(expected.fits.length).toBeGreaterThan(0);
236
-
237
- const element = await Page({
238
- params: Promise.resolve({ locale: "en", slug: SLUG, payload }),
239
- });
240
- render(element);
241
-
242
- expect(mockRedirect).not.toHaveBeenCalled();
243
- expect(screen.getByTestId("flow-results")).toBeInTheDocument();
244
- expect(
245
- screen.getByRole("heading", {
246
- name: resolveLocalizedString(
247
- config.recommendation!.copy.title,
248
- "en",
249
- config.defaultLocale,
250
- ),
251
- }),
252
- ).toBeInTheDocument();
253
-
254
- for (const fit of expected.fits) {
255
- const offer = config.offers!.find((o) => o.key === fit.offerKey)!;
256
- const name = resolveLocalizedString(
257
- offer.copy.name,
258
- "en",
259
- config.defaultLocale,
260
- );
261
- // #4842 FR1 — the top fit's name is ALSO the safe-default side panel's
262
- // `offerName`, so the FIRST fit renders exactly twice (panel + card)
263
- // and every other fit exactly once.
264
- expect(screen.getAllByText(name)).toHaveLength(
265
- fit.offerKey === expected.fits[0]!.offerKey ? 2 : 1,
266
- );
267
- if (offer.copy.description) {
268
- expect(
269
- screen.getByText(
270
- resolveLocalizedString(
271
- offer.copy.description,
272
- "en",
273
- config.defaultLocale,
274
- ),
275
- ),
276
- ).toBeInTheDocument();
277
- }
278
-
279
- // The matched reason renders inside the "Why this?" Popover
280
- // disclosure — reveal it (mirrors packages/ui/src/blocks/__tests__/
281
- // RecommendationCard.test.tsx's own click-to-reveal pattern).
282
- await user.click(
283
- screen.getByTestId(`recommendation-card-${fit.offerKey}-why-trigger`),
284
- );
285
- expect(
286
- await screen.findByText(
287
- resolveLocalizedString(fit.reason, "en", config.defaultLocale),
288
- ),
289
- ).toBeInTheDocument();
290
- }
291
- });
292
- });
293
-
294
- describe("generateMetadata", () => {
295
- it("resolves the page title from the compiled recommendation marker", async () => {
296
- const payload = encodePermalink({
297
- version: PERMALINK_VERSION,
298
- flowSlug: SLUG,
299
- answers: KNOWN_ANSWERS,
300
- });
301
- mockGet.mockResolvedValueOnce({ ok: true, body: fixtureConfig() });
302
-
303
- const metadata = await generateMetadata({
304
- params: Promise.resolve({ locale: "en", slug: SLUG, payload }),
305
- });
306
- expect(metadata.title).toBe("Your recommendation");
307
- });
308
-
309
- it("returns an empty metadata object for an invalid payload", async () => {
310
- const metadata = await generateMetadata({
311
- params: Promise.resolve({
312
- locale: "en",
313
- slug: SLUG,
314
- payload: "garbage",
315
- }),
316
- });
317
- expect(metadata).toEqual({});
318
- expect(mockGet).not.toHaveBeenCalled();
319
- });
320
- });
321
-
322
- // ─── #4842 AC-5 — the `?checkout=` return states (additive; the cases above
323
- // are the regression floor and stay as they were) ───────────────────────────
324
-
325
- describe("AC-5 (#4842) — ?checkout= return states render over the same recommendation", () => {
326
- function validPayload(): string {
327
- return encodePermalink({
328
- version: PERMALINK_VERSION,
329
- flowSlug: SLUG,
330
- answers: KNOWN_ANSWERS,
331
- });
332
- }
333
-
334
- async function renderWithSearch(
335
- checkout: string | undefined,
336
- ): Promise<ReturnType<typeof recommend>> {
337
- const config = fixtureConfig();
338
- mockGet.mockResolvedValueOnce({ ok: true, body: config });
339
- const expected = recommend(
340
- {
341
- key: SLUG,
342
- offers: config.offers,
343
- pairings: config.pairings,
344
- rateTable: config.rateTable,
345
- scoring: config.scoring,
346
- },
347
- KNOWN_ANSWERS,
348
- );
349
- const element = await Page({
350
- params: Promise.resolve({ locale: "en", slug: SLUG, payload: validPayload() }),
351
- searchParams: Promise.resolve(checkout === undefined ? {} : { checkout }),
352
- });
353
- render(element);
354
- expect(mockRedirect).not.toHaveBeenCalled();
355
- return expected;
356
- }
357
-
358
- function expectIdenticalFits(expected: ReturnType<typeof recommend>): void {
359
- expect(expected.fits.length).toBeGreaterThan(0);
360
- for (const fit of expected.fits) {
361
- expect(screen.getByTestId(`recommendation-card-${fit.offerKey}`)).toBeInTheDocument();
362
- }
363
- expect(screen.getAllByTestId(/^recommendation-card-[a-z_]+$/)).toHaveLength(
364
- expected.fits.length,
365
- );
366
- }
367
-
368
- it("?checkout=success renders the success status line AND the identical fit cards", async () => {
369
- const expected = await renderWithSearch("success");
370
- expect(screen.getByTestId("flow-results-return-state")).toHaveTextContent("checkoutSuccess");
371
- expect(screen.getByRole("status")).toBeInTheDocument();
372
- expectIdenticalFits(expected);
373
- });
374
-
375
- it("?checkout=cancelled renders the cancelled status line AND the identical fit cards", async () => {
376
- const expected = await renderWithSearch("cancelled");
377
- expect(screen.getByTestId("flow-results-return-state")).toHaveTextContent("checkoutCancelled");
378
- expectIdenticalFits(expected);
379
- });
380
-
381
- it("an unrecognised value renders no status line; the fits are unchanged", async () => {
382
- const expected = await renderWithSearch("bogus");
383
- expect(screen.queryByTestId("flow-results-return-state")).not.toBeInTheDocument();
384
- expectIdenticalFits(expected);
385
- });
386
-
387
- it("a return param never changes which redirect guard fires (invalid payload still redirects to step 1)", async () => {
388
- await expect(
389
- Page({
390
- params: Promise.resolve({ locale: "en", slug: SLUG, payload: "not-a-valid-payload!!!" }),
391
- searchParams: Promise.resolve({ checkout: "success" }),
392
- }),
393
- ).rejects.toThrow(`NEXT_REDIRECT:/en/flows/${SLUG}`);
394
- expect(mockGet).not.toHaveBeenCalled();
395
- });
396
- });
@@ -1,114 +0,0 @@
1
- /**
2
- * /[locale]/flows/[slug]/results/[payload] — the cold-load results route
3
- * (#4770).
4
- *
5
- * Server-fetches the flow config exactly as `flows/[slug]/page.tsx` does
6
- * (`flows.get(slug)`, always dynamically rendered — the config is DB-backed
7
- * live data).
8
- * Decodes the versioned, answers-only permalink (`decodePermalink`,
9
- * `@working-theory/validation`) and `redirect()`s to `/[locale]/flows/[slug]`
10
- * — step 1, never an error page — on ANY of: an invalid payload (FR6), a
11
- * stale-version payload (FR6), a payload whose own `flowSlug` does not match
12
- * this route's `[slug]` (D2/FR6), or a served flow that is not a
13
- * recommending register (D8 — `mode !== "recommending" || !offers`, checked
14
- * against the field `recommend()` is about to be handed rather than
15
- * inferring the compiler's five-keys-travel-together guarantee).
16
- *
17
- * On a valid payload against a recommending flow, renders `ResultsClient`,
18
- * which runs the already-shipped pure `recommend()` in the browser and
19
- * renders the returned fits through the existing `RecommendationCard`
20
- * (D10 — a minimal render; Epic #4759 recomposes it against the LOCKED
21
- * DESIGN.md).
22
- *
23
- * `?checkout=success|cancelled` (#4842 FR7/D6) is read only after every
24
- * guard above and rendered by `ResultsClient` as a one-line status over the
25
- * identical recommendation.
26
- */
27
- import { flows } from "@working-theory/api-client";
28
- import {
29
- decodePermalink,
30
- resolveLocalizedString,
31
- } from "@working-theory/validation";
32
- import type { Metadata } from "next";
33
- import { redirect } from "next/navigation";
34
- import type { ReactElement } from "react";
35
-
36
- import type { ResultsReturnState } from "./ResultsClient";
37
- import { ResultsClient } from "./ResultsClient";
38
-
39
- export const dynamic = "force-dynamic";
40
-
41
- interface Props {
42
- params: Promise<{ locale: string; slug: string; payload: string }>;
43
- /** FR7 (#4842) — `?checkout=success|cancelled` after Stripe's hosted page. */
44
- searchParams?: Promise<Record<string, string | string[] | undefined>>;
45
- }
46
-
47
- /**
48
- * D6 (#4842) — the return micro-state, validated against the two literals;
49
- * anything else (absent, an array, an unknown value) renders no status line.
50
- */
51
- function resolveReturnState(
52
- value: string | string[] | undefined,
53
- ): ResultsReturnState | undefined {
54
- return value === "success" || value === "cancelled" ? value : undefined;
55
- }
56
-
57
- export async function generateMetadata({ params }: Props): Promise<Metadata> {
58
- const { locale, slug, payload } = await params;
59
-
60
- const decoded = decodePermalink(payload);
61
- if (!decoded.ok || decoded.value.flowSlug !== slug) return {};
62
-
63
- const res = await flows.get(slug);
64
- if (!res.ok) return {};
65
-
66
- const { recommendation } = res.body;
67
- if (!recommendation) return {};
68
-
69
- return {
70
- title: resolveLocalizedString(
71
- recommendation.copy.title,
72
- locale,
73
- res.body.defaultLocale,
74
- ),
75
- };
76
- }
77
-
78
- export default async function Page({
79
- params,
80
- searchParams,
81
- }: Props): Promise<ReactElement> {
82
- const { locale, slug, payload } = await params;
83
-
84
- const decoded = decodePermalink(payload);
85
- if (!decoded.ok) {
86
- redirect(`/${locale}/flows/${slug}`);
87
- }
88
- if (decoded.value.flowSlug !== slug) {
89
- redirect(`/${locale}/flows/${slug}`);
90
- }
91
-
92
- const res = await flows.get(slug);
93
- if (!res.ok) {
94
- redirect(`/${locale}/flows/${slug}`);
95
- }
96
- if (res.body.mode !== "recommending" || res.body.offers === undefined) {
97
- redirect(`/${locale}/flows/${slug}`);
98
- }
99
-
100
- // FR7 — read AFTER every redirect guard above, so a return param never
101
- // changes which guard fires or where it sends the visitor.
102
- const returnState = resolveReturnState((await searchParams)?.checkout);
103
-
104
- return (
105
- <ResultsClient
106
- config={res.body}
107
- locale={locale}
108
- answers={decoded.value.answers}
109
- slug={slug}
110
- payload={payload}
111
- {...(returnState !== undefined ? { returnState } : {})}
112
- />
113
- );
114
- }
@@ -1,110 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * ListingApplicationClient — the REFERENCE MOUNT for the founder application
5
- * (#5048).
6
- *
7
- * This file is the whole per-site cost of the consumer surface, and it is
8
- * deliberately this short. Before it, a site had to hand-write three proxy
9
- * routes, a form that saved on every advance, and a decision about where a
10
- * bearer token lives. What is left is: bind `projectListings` to the section,
11
- * and decide what to do with the token when `start` mints one.
12
- *
13
- * IT IS NOT A BFF, and a site must not copy it into one (the placement
14
- * decision's D4). `apps/api` is called DIRECTLY: an agent-mediated caller
15
- * (#5039) cannot use any site's proxy, so the direct path has to be safe on
16
- * its own terms regardless — and it is, because `siteId` is resolved
17
- * server-side from the public site key rather than taken from the body. A
18
- * proxy would be convenience, not a security boundary.
19
- *
20
- * TOKEN CUSTODY (#5048 (a)). The token is caller-held. This mount reads a
21
- * resume link's `id` and `token` query parameters and passes them straight to
22
- * the section; when `start` mints a new pair it replaces the URL's query
23
- * parameters with them, so a founder who reloads or navigates back keeps their
24
- * draft. That is a deliberate choice with a cost: a token in a URL lands in
25
- * browser history and in any `Referer` the page sends. It is the accepted cost
26
- * of a resume link that has to survive being EMAILED and opened in a cold
27
- * browser (samhenry #316 D6b), which is the constraint that rules out an
28
- * httpOnly cookie entirely. A site with different constraints replaces this
29
- * one function; nothing else here changes.
30
- *
31
- * NO ANTI-AUTOMATION CONTROLS. No honeypot, no timing check, deliberately —
32
- * see #316 D6b and #5039.
33
- *
34
- * SHIPS NO COPY, AND NO ROUTE. Every string comes from the `config` and
35
- * `messages` a caller passes in. There is deliberately no `page.tsx` beside
36
- * this file: a route would need a question set, and a question set is the
37
- * SITE's content — shipping a placeholder one here would put founder-readable
38
- * copy in the template, which is exactly what this unit exists to stop. A site
39
- * adds its own route, imports this wrapper, and passes its own config.
40
- */
41
-
42
- import { projectListings } from "@working-theory/api-client";
43
- import { ListingApplicationFlow } from "@working-theory/ui";
44
- import type {
45
- FlowStepperMessages,
46
- ListingApplicationDraftClient,
47
- ListingApplicationMessages,
48
- } from "@working-theory/ui";
49
- import type { ListingApplicationConfig } from "@working-theory/validation";
50
- import { useRouter, useSearchParams } from "next/navigation";
51
- import type { ReactElement, ReactNode } from "react";
52
-
53
- /**
54
- * `projectListings` satisfies the section's structural client contract as-is —
55
- * `start`, `patchDraft` and `submit` already have the right shapes, and the
56
- * section defines the interface rather than importing this package so that
57
- * `packages/ui` stays transport-agnostic (#3957 D2).
58
- */
59
- const client: ListingApplicationDraftClient = projectListings;
60
-
61
- /** The query parameters an emailed resume link carries. */
62
- const DRAFT_ID_PARAM = "draft";
63
- const RESUME_TOKEN_PARAM = "token";
64
-
65
- interface Props {
66
- config: ListingApplicationConfig;
67
- locale: string;
68
- messages: FlowStepperMessages & ListingApplicationMessages;
69
- /** Rendered under the live tile — the per-listing privacy notice goes here. */
70
- tileFooter?: ReactNode;
71
- }
72
-
73
- export function ListingApplicationClient({
74
- config,
75
- locale,
76
- messages,
77
- tileFooter,
78
- }: Props): ReactElement {
79
- const router = useRouter();
80
- const params = useSearchParams();
81
-
82
- const draftId = params.get(DRAFT_ID_PARAM);
83
- const resumeToken = params.get(RESUME_TOKEN_PARAM);
84
- const resume =
85
- draftId && resumeToken ? { id: draftId, resumeToken } : undefined;
86
-
87
- return (
88
- <ListingApplicationFlow
89
- config={config}
90
- locale={locale}
91
- messages={messages}
92
- client={client}
93
- {...(resume ? { resume } : {})}
94
- {...(tileFooter !== undefined ? { tileFooter } : {})}
95
- onDraftStarted={({ id, resumeToken: token }) => {
96
- // Put the pair in the URL so a reload or a back-navigation resumes
97
- // rather than starting a second draft. `replace`, not `push`: the
98
- // founder should not have to walk back through one history entry per
99
- // step. The site's own email send goes here too — the same two values
100
- // are what its resume link carries.
101
- const next = new URLSearchParams(params.toString());
102
- next.set(DRAFT_ID_PARAM, id);
103
- next.set(RESUME_TOKEN_PARAM, token);
104
- router.replace(`?${next.toString()}`, { scroll: false });
105
- }}
106
- />
107
- );
108
- }
109
-
110
- export default ListingApplicationClient;