create-ailk 0.7.0 → 0.9.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/README.md +112 -10
- package/component-catalog.json +798 -49
- package/component-catalog.md +9 -6
- package/dist/add-module.d.ts +20 -0
- package/dist/add-module.js +43 -7
- package/dist/add-route.d.ts +56 -0
- package/dist/add-route.js +286 -0
- package/dist/apply-design-md.d.ts +7 -0
- package/dist/apply-design-md.js +50 -0
- package/dist/apply-design.d.ts +64 -0
- package/dist/apply-design.js +273 -0
- package/dist/claude-md.d.ts +111 -0
- package/dist/claude-md.js +334 -0
- package/dist/cli.js +114 -28
- package/dist/derive-architecture.js +41 -36
- package/dist/design-compile.d.ts +71 -0
- package/dist/design-compile.js +98 -0
- package/dist/design-package.d.ts +83 -0
- package/dist/design-package.js +468 -0
- package/dist/design-presets.d.ts +86 -0
- package/dist/design-presets.js +642 -0
- package/dist/design-scaffold.d.ts +76 -0
- package/dist/design-scaffold.js +220 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/legacy-nav-renderers.d.ts +45 -0
- package/dist/legacy-nav-renderers.js +545 -0
- package/dist/lib/auth-secret-file.d.ts +19 -0
- package/dist/lib/auth-secret-file.js +112 -0
- package/dist/lib/design-checks.d.ts +78 -0
- package/dist/lib/design-checks.js +174 -0
- package/dist/lib/extract-module-bundle.js +5 -1
- package/dist/lib/license-key-files.d.ts +9 -0
- package/dist/lib/license-key-files.js +2 -2
- package/dist/lib/module-license-gate.d.ts +89 -0
- package/dist/lib/module-license-gate.js +171 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- package/dist/lib/render-verdict.d.ts +52 -0
- package/dist/lib/render-verdict.js +77 -0
- package/dist/lib/scaffold-render-sweep.d.ts +105 -0
- package/dist/lib/scaffold-render-sweep.js +236 -0
- package/dist/module-architecture.d.ts +19 -16
- package/dist/module-architecture.js +64 -21
- package/dist/parse-args.d.ts +88 -10
- package/dist/parse-args.js +272 -21
- package/dist/programmatic.d.ts +66 -12
- package/dist/programmatic.js +361 -34
- package/dist/site-chrome.d.ts +19 -9
- package/dist/site-chrome.js +23 -13
- package/dist/site-manifest.d.ts +25 -0
- package/dist/site-manifest.js +254 -0
- package/dist/site-roster.d.ts +41 -0
- package/dist/site-roster.js +149 -11
- package/dist/starter-grid.d.ts +232 -0
- package/dist/starter-grid.js +545 -0
- package/dist/surfaces.d.ts +41 -9
- package/dist/surfaces.js +268 -92
- package/dist/template-manifest.d.ts +65 -29
- package/dist/template-manifest.js +161 -382
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +5 -4
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
- package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
- package/site-templates/designs/minimalism/DESIGN.md +98 -6
- package/site-templates/designs/minimalism/sample/README.md +48 -0
- package/site-templates/designs/standard/DESIGN.md +103 -6
- package/site-templates/marketing-site/DESIGN.md +103 -6
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
- package/site-templates/marketing-site-free/DESIGN.md +103 -6
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/saas-product/DESIGN.md +103 -6
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +471 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/rules/architecture.md +7 -0
- package/templates/.claude/skills/README.md +5 -3
- package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
- package/templates/.claude/skills/add-page/SKILL.md +241 -238
- package/templates/.claude/skills/add-page/references/page-types.md +85 -72
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
- package/templates/.claude/skills/check-site/SKILL.md +23 -0
- package/templates/.claude/skills/fill-page/SKILL.md +26 -7
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- package/templates/.claude/skills/update-page/SKILL.md +117 -0
- package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
- package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/.prettierignore +1 -0
- package/templates/CLAUDE.md +146 -103
- package/templates/DESIGN.md +103 -6
- package/templates/README.md +63 -43
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
- package/templates/apps/api/src/__tests__/server.test.ts +116 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/lib/stripe.ts +9 -0
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
- package/templates/apps/api/src/middleware/auth.ts +54 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
- package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
- package/templates/apps/api/src/server.ts +35 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
- package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/api/src/services/revalidation.ts +60 -15
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/mcp/src/tools/purchase.ts +4 -2
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/CLAUDE.md +22 -28
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
- package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +21 -5
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
- package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +65 -17
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
- package/templates/apps/web/app/api/revalidate/route.ts +78 -36
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
- package/templates/apps/web/lib/locale-guard.ts +18 -0
- package/templates/apps/web/lib/meet-placeholder.ts +5 -0
- package/templates/apps/web/lib/nav-config.ts +50 -37
- package/templates/apps/web/lib/site-brand.tsx +13 -9
- package/templates/apps/web/lib/site-identity.ts +14 -0
- package/templates/apps/web/lib/site-logo.tsx +36 -0
- package/templates/apps/web/lib/site-theme.ts +46 -26
- package/templates/apps/web/messages/en.json +45 -5
- package/templates/apps/web/middleware.ts +28 -4
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- package/templates/content/en/BookCall/book-a-call.mdx +80 -0
- package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
- package/templates/content/en/BookCall/onsite-service.mdx +56 -0
- package/templates/content/en/BookCall/start-project.mdx +56 -0
- package/templates/content/en/ContactPage/contact.mdx +43 -34
- package/templates/content/en/HomePage/home.mdx +10 -22
- package/templates/content/en/LandingPage/meet.mdx +59 -0
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +2 -1
- package/templates/tools/check-claude-md.ts +128 -0
- package/versions.json +33 -33
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/minimalism-site/DESIGN.md +0 -162
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
- package/site-templates/minimalism-site/ailk-template.json +0 -23
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
- package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
- package/site-templates/minimalism-site/provenance.json +0 -4
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
- package/templates/content/en/SoftwareProduct/product.mdx +0 -175
- /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: "ContactPage"
|
|
3
|
-
slug: "contact"
|
|
4
|
-
title: "Contact"
|
|
5
|
-
description: "One form, three fields, an inline confirmation. Placeholder copy — replace before you launch."
|
|
6
|
-
sections:
|
|
7
|
-
[
|
|
8
|
-
{
|
|
9
|
-
"id": "hero",
|
|
10
|
-
"type": "hero",
|
|
11
|
-
"data":
|
|
12
|
-
{
|
|
13
|
-
"variant": "centered",
|
|
14
|
-
"eyebrow": "Contact",
|
|
15
|
-
"headline": "Tell us about the yard",
|
|
16
|
-
"subheadline": "{One line on what happens after you send this, and how long it takes. If a call is faster, say so.}",
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"id": "closing",
|
|
21
|
-
"type": "cta",
|
|
22
|
-
"data":
|
|
23
|
-
{
|
|
24
|
-
"variant": "minimal",
|
|
25
|
-
"headline": "Would a visit be faster?",
|
|
26
|
-
"primaryCta": { "label": "Book a visit →", "href": "/meet" },
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
]
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
The /contact route mounts the page's one contact surface — the guided intake, with its fallback form — which posts to the site's own API (`/v1/leads/submit`) and confirms inline. Placeholder copy.
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: "FAQPage"
|
|
3
|
-
slug: "faq"
|
|
4
|
-
title: "FAQ"
|
|
5
|
-
description: "The questions people ask before they book. Placeholder copy — replace every answer before you launch."
|
|
6
|
-
sections:
|
|
7
|
-
[
|
|
8
|
-
{
|
|
9
|
-
"id": "faq",
|
|
10
|
-
"type": "faq",
|
|
11
|
-
"data":
|
|
12
|
-
{
|
|
13
|
-
"headline": "Questions",
|
|
14
|
-
"variant": "centered",
|
|
15
|
-
"initialOpen": "first",
|
|
16
|
-
"items":
|
|
17
|
-
[
|
|
18
|
-
{
|
|
19
|
-
"question": "{Which areas do you cover?}",
|
|
20
|
-
"answer": "{Name the towns, the radius or the postcodes. A reader outside them should know it here.}",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"question": "{How often do you visit?}",
|
|
24
|
-
"answer": "{Give the real cadence for the recurring job, and say what happens in winter.}",
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"question": "{What does it cost?}",
|
|
28
|
-
"answer": "{The three service pages carry the prices. Say here what moves them — yard size, access, waste removal.}",
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"question": "{How do I pay?}",
|
|
32
|
-
"answer": "{Say when payment is taken and how. Checkout runs on your own Stripe account; the card details never touch this site.}",
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"question": "{What if I need to cancel?}",
|
|
36
|
-
"answer": "{Point at the cancellation policy and give the notice period in one line.}",
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
"supportLink":
|
|
40
|
-
{ "label": "Still stuck? Get in touch", "href": "/contact" },
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"id": "closing",
|
|
45
|
-
"type": "cta",
|
|
46
|
-
"data":
|
|
47
|
-
{
|
|
48
|
-
"variant": "minimal",
|
|
49
|
-
"headline": "Still have a question?",
|
|
50
|
-
"primaryCta": { "label": "Contact us →", "href": "/contact" },
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
]
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
Placeholder copy. Replace every answer before you launch.
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: "HomePage"
|
|
3
|
-
slug: "home"
|
|
4
|
-
title: "Your Company"
|
|
5
|
-
description: "Placeholder home for the simple-service template — a lawn-care site that books a visit and takes the payment. Replace every line before you launch."
|
|
6
|
-
sections:
|
|
7
|
-
[
|
|
8
|
-
{
|
|
9
|
-
"id": "hero",
|
|
10
|
-
"type": "hero",
|
|
11
|
-
"data":
|
|
12
|
-
{
|
|
13
|
-
"variant": "centered",
|
|
14
|
-
"eyebrow": "{Your service area}",
|
|
15
|
-
"headline": "{Lawn care that turns up when it says it will}",
|
|
16
|
-
"subheadline": "{One line on who you serve and what they get. Name the street, the town or the county — a local reader is looking for their own.}",
|
|
17
|
-
"primaryCta": { "label": "Book a visit →", "href": "/meet" },
|
|
18
|
-
"secondaryCta":
|
|
19
|
-
{
|
|
20
|
-
"label": "See services",
|
|
21
|
-
"href": "/services",
|
|
22
|
-
"variant": "outline",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"id": "services",
|
|
28
|
-
"type": "items",
|
|
29
|
-
"data":
|
|
30
|
-
{
|
|
31
|
-
"headline": "What we do",
|
|
32
|
-
"columns": 3,
|
|
33
|
-
"items":
|
|
34
|
-
[
|
|
35
|
-
{
|
|
36
|
-
"title": "{Lawn maintenance}",
|
|
37
|
-
"description": "{One line on the recurring visit — what it includes and how often it runs.}",
|
|
38
|
-
"link":
|
|
39
|
-
{
|
|
40
|
-
"label": "Read more",
|
|
41
|
-
"href": "/services/lawn-maintenance",
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"title": "{Seasonal cleanup}",
|
|
46
|
-
"description": "{One line on the spring and autumn clear-down.}",
|
|
47
|
-
"link":
|
|
48
|
-
{
|
|
49
|
-
"label": "Read more",
|
|
50
|
-
"href": "/services/seasonal-cleanup",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"title": "{Landscape design}",
|
|
55
|
-
"description": "{One line on the design-and-plant job, and what a customer ends up with.}",
|
|
56
|
-
"link":
|
|
57
|
-
{
|
|
58
|
-
"label": "Read more",
|
|
59
|
-
"href": "/services/landscape-design",
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"id": "closing",
|
|
67
|
-
"type": "cta",
|
|
68
|
-
"data":
|
|
69
|
-
{
|
|
70
|
-
"variant": "minimal",
|
|
71
|
-
"headline": "Ready for a tidier yard?",
|
|
72
|
-
"description": "{One line inviting the reader to pick a time rather than fill in a form.}",
|
|
73
|
-
"primaryCta": { "label": "Book a visit →", "href": "/meet" },
|
|
74
|
-
"secondaryCta":
|
|
75
|
-
{
|
|
76
|
-
"label": "Ask a question",
|
|
77
|
-
"href": "/contact",
|
|
78
|
-
"variant": "outline",
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
]
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
Placeholder copy. Every headline, every line of body copy and every price on this site is a placeholder — replace them before you launch.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: "Service"
|
|
3
|
-
slug: "landscape-design"
|
|
4
|
-
title: "{Landscape design}"
|
|
5
|
-
description: "{One line on what this job is and who it suits. Replace before you launch.}"
|
|
6
|
-
price: 1250
|
|
7
|
-
priceCurrency: "USD"
|
|
8
|
-
provider:
|
|
9
|
-
name: "{Your Company}"
|
|
10
|
-
schema:
|
|
11
|
-
serviceType: "{Landscape design}"
|
|
12
|
-
areaServed: "{Your service area}"
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
# \{Landscape design\}
|
|
16
|
-
|
|
17
|
-
\{Open with the situation a reader is in when they come looking for this. Two or
|
|
18
|
-
three sentences, no more — the detail belongs under the headings below.\}
|
|
19
|
-
|
|
20
|
-
## What it covers
|
|
21
|
-
|
|
22
|
-
- \{The first thing you actually do\}
|
|
23
|
-
- \{The second thing you actually do\}
|
|
24
|
-
- \{The third thing you actually do\}
|
|
25
|
-
|
|
26
|
-
## How it runs
|
|
27
|
-
|
|
28
|
-
\{One paragraph on the shape of the visit: how it is scheduled, how long it
|
|
29
|
-
takes, who turns up, and what you need from the customer beforehand.\}
|
|
30
|
-
|
|
31
|
-
## What you end up with
|
|
32
|
-
|
|
33
|
-
\{One paragraph on the result. Be concrete — describe the yard when the crew
|
|
34
|
-
drives away.\}
|
|
35
|
-
|
|
36
|
-
## What it costs
|
|
37
|
-
|
|
38
|
-
\{The price in this page's frontmatter is a PLACEHOLDER. It is what the site
|
|
39
|
-
publishes as a schema.org `Offer`, so an agent reading this page quotes it —
|
|
40
|
-
replace it with your real number before you launch.\}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: "Service"
|
|
3
|
-
slug: "lawn-maintenance"
|
|
4
|
-
title: "{Lawn maintenance}"
|
|
5
|
-
description: "{One line on what this job is and who it suits. Replace before you launch.}"
|
|
6
|
-
price: 149
|
|
7
|
-
priceCurrency: "USD"
|
|
8
|
-
provider:
|
|
9
|
-
name: "{Your Company}"
|
|
10
|
-
schema:
|
|
11
|
-
serviceType: "{Lawn maintenance}"
|
|
12
|
-
areaServed: "{Your service area}"
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
# \{Lawn maintenance\}
|
|
16
|
-
|
|
17
|
-
\{Open with the situation a reader is in when they come looking for this. Two or
|
|
18
|
-
three sentences, no more — the detail belongs under the headings below.\}
|
|
19
|
-
|
|
20
|
-
## What it covers
|
|
21
|
-
|
|
22
|
-
- \{The first thing you actually do\}
|
|
23
|
-
- \{The second thing you actually do\}
|
|
24
|
-
- \{The third thing you actually do\}
|
|
25
|
-
|
|
26
|
-
## How it runs
|
|
27
|
-
|
|
28
|
-
\{One paragraph on the shape of the visit: how it is scheduled, how long it
|
|
29
|
-
takes, who turns up, and what you need from the customer beforehand.\}
|
|
30
|
-
|
|
31
|
-
## What you end up with
|
|
32
|
-
|
|
33
|
-
\{One paragraph on the result. Be concrete — describe the yard when the crew
|
|
34
|
-
drives away.\}
|
|
35
|
-
|
|
36
|
-
## What it costs
|
|
37
|
-
|
|
38
|
-
\{The price in this page's frontmatter is a PLACEHOLDER. It is what the site
|
|
39
|
-
publishes as a schema.org `Offer`, so an agent reading this page quotes it —
|
|
40
|
-
replace it with your real number before you launch.\}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: "Service"
|
|
3
|
-
slug: "seasonal-cleanup"
|
|
4
|
-
title: "{Seasonal cleanup}"
|
|
5
|
-
description: "{One line on what this job is and who it suits. Replace before you launch.}"
|
|
6
|
-
price: 289
|
|
7
|
-
priceCurrency: "USD"
|
|
8
|
-
provider:
|
|
9
|
-
name: "{Your Company}"
|
|
10
|
-
schema:
|
|
11
|
-
serviceType: "{Seasonal cleanup}"
|
|
12
|
-
areaServed: "{Your service area}"
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
# \{Seasonal cleanup\}
|
|
16
|
-
|
|
17
|
-
\{Open with the situation a reader is in when they come looking for this. Two or
|
|
18
|
-
three sentences, no more — the detail belongs under the headings below.\}
|
|
19
|
-
|
|
20
|
-
## What it covers
|
|
21
|
-
|
|
22
|
-
- \{The first thing you actually do\}
|
|
23
|
-
- \{The second thing you actually do\}
|
|
24
|
-
- \{The third thing you actually do\}
|
|
25
|
-
|
|
26
|
-
## How it runs
|
|
27
|
-
|
|
28
|
-
\{One paragraph on the shape of the visit: how it is scheduled, how long it
|
|
29
|
-
takes, who turns up, and what you need from the customer beforehand.\}
|
|
30
|
-
|
|
31
|
-
## What you end up with
|
|
32
|
-
|
|
33
|
-
\{One paragraph on the result. Be concrete — describe the yard when the crew
|
|
34
|
-
drives away.\}
|
|
35
|
-
|
|
36
|
-
## What it costs
|
|
37
|
-
|
|
38
|
-
\{The price in this page's frontmatter is a PLACEHOLDER. It is what the site
|
|
39
|
-
publishes as a schema.org `Offer`, so an agent reading this page quotes it —
|
|
40
|
-
replace it with your real number before you launch.\}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Emitted by `add-page` at `app/{{SLUG}}/page.tsx`. Substitutions:
|
|
2
|
-
// {{SLUG}} — page slug (e.g., "pricing")
|
|
3
|
-
// {{TYPE}} — schema.org @type (e.g., "Service")
|
|
4
|
-
// {{BUILDER}} — typed builder name (e.g., "buildService")
|
|
5
|
-
// {{TITLE}} — page title (e.g., "Pricing")
|
|
6
|
-
import { notFound } from "next/navigation";
|
|
7
|
-
|
|
8
|
-
import { JsonLd } from "@working-theory/jsonld";
|
|
9
|
-
import { {{BUILDER}} } from "@working-theory/schema";
|
|
10
|
-
import { mdxAdapter } from "@/lib/content";
|
|
11
|
-
|
|
12
|
-
export default async function {{TYPE}}Page() {
|
|
13
|
-
const page = await mdxAdapter.getPage("{{SLUG}}");
|
|
14
|
-
if (!page) return notFound();
|
|
15
|
-
|
|
16
|
-
const schema = {{BUILDER}}(page);
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<main className="mx-auto max-w-3xl px-6 py-12">
|
|
20
|
-
<JsonLd schema={schema} />
|
|
21
|
-
<article className="prose dark:prose-invert">
|
|
22
|
-
<h1>{page.title}</h1>
|
|
23
|
-
{page.description ? <p className="lead">{page.description}</p> : null}
|
|
24
|
-
{/* Render MDX body via your project's MDX renderer of choice. */}
|
|
25
|
-
</article>
|
|
26
|
-
</main>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ContactForm — client component that wires the contact form to
|
|
5
|
-
* captureLeadAction via useFormState.
|
|
6
|
-
*
|
|
7
|
-
* Validation errors render inline next to each field.
|
|
8
|
-
* Success shows a confirmation message.
|
|
9
|
-
* Server errors show a generic fallback message.
|
|
10
|
-
*
|
|
11
|
-
* Honeypot + rate-limit enforcement lives in apps/api; no duplication here.
|
|
12
|
-
*
|
|
13
|
-
* `initialValues` (#4534, epic #4521's integration leg): optional prefill
|
|
14
|
-
* for name/email/message, set via `defaultValue` (the fields stay
|
|
15
|
-
* uncontrolled — unchanged for the no-prop call site). Lets
|
|
16
|
-
* `ConversationalIntakeWrapper` carry over whatever the visitor already
|
|
17
|
-
* typed into the conversation before Skip (or a failed auto-submit)
|
|
18
|
-
* dropped them here, instead of asking them to retype it.
|
|
19
|
-
*
|
|
20
|
-
* Consent (#6466): a real checkbox named by a `ConsentStatement` whose
|
|
21
|
-
* "Terms" links to the locale-aware `/terms` route. It starts unticked and
|
|
22
|
-
* the submit button stays disabled until it is ticked — the form is
|
|
23
|
-
* `noValidate`, so `required` alone would gate nothing. The statement is
|
|
24
|
-
* wired by `aria-labelledby`, never a wrapping `<label>`, so clicking the
|
|
25
|
-
* link follows it instead of toggling the box (the ConsentGroup rule).
|
|
26
|
-
*
|
|
27
|
-
* Composition (#6514): every control and message is a `@working-theory/ui`
|
|
28
|
-
* primitive — `Label`/`Input`/`Textarea` for the fields, `Alert` for the
|
|
29
|
-
* success and form-level error, `InlineMessage` for the per-field messages
|
|
30
|
-
* (its `error` variant carries `role="alert"`), `Button` for submit. The honeypot hides behind
|
|
31
|
-
* the `hidden` attribute on its wrapper rather than an inline style object.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import {
|
|
35
|
-
Alert,
|
|
36
|
-
AlertDescription,
|
|
37
|
-
Button,
|
|
38
|
-
Checkbox,
|
|
39
|
-
type CheckboxProps,
|
|
40
|
-
ConsentStatement,
|
|
41
|
-
InlineMessage,
|
|
42
|
-
Input,
|
|
43
|
-
Label,
|
|
44
|
-
localeHref,
|
|
45
|
-
Textarea,
|
|
46
|
-
} from "@working-theory/ui";
|
|
47
|
-
import { useLocale, useTranslations } from "next-intl";
|
|
48
|
-
import { useCallback, useState } from "react";
|
|
49
|
-
import { useFormState, useFormStatus } from "react-dom";
|
|
50
|
-
|
|
51
|
-
import { captureLeadAction } from "./contact.actions";
|
|
52
|
-
|
|
53
|
-
export interface ContactFormInitialValues {
|
|
54
|
-
name?: string;
|
|
55
|
-
email?: string;
|
|
56
|
-
message?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type CheckedState = Parameters<NonNullable<CheckboxProps["onCheckedChange"]>>[0];
|
|
60
|
-
|
|
61
|
-
interface ContactFormProps {
|
|
62
|
-
initialValues?: ContactFormInitialValues;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function SubmitButton({ disabled }: { disabled: boolean }) {
|
|
66
|
-
const t = useTranslations("contact");
|
|
67
|
-
const { pending } = useFormStatus();
|
|
68
|
-
return (
|
|
69
|
-
<Button
|
|
70
|
-
type="submit"
|
|
71
|
-
disabled={pending || disabled}
|
|
72
|
-
data-testid="contact-submit"
|
|
73
|
-
>
|
|
74
|
-
{pending ? t("sending") : t("sendMessage")}
|
|
75
|
-
</Button>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function ContactForm({ initialValues }: ContactFormProps = {}) {
|
|
80
|
-
const t = useTranslations("contact");
|
|
81
|
-
const locale = useLocale();
|
|
82
|
-
const [state, formAction] = useFormState(captureLeadAction, null);
|
|
83
|
-
const [consented, setConsented] = useState(false);
|
|
84
|
-
const handleConsentChange = useCallback((checked: CheckedState) => {
|
|
85
|
-
setConsented(checked === true);
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
if (state?.kind === "success") {
|
|
89
|
-
return (
|
|
90
|
-
<Alert variant="success" data-testid="contact-success">
|
|
91
|
-
<AlertDescription>{t("successMessage")}</AlertDescription>
|
|
92
|
-
</Alert>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const fieldErrors = state?.kind === "error" ? (state.fieldErrors ?? {}) : {};
|
|
97
|
-
const formError =
|
|
98
|
-
state?.kind === "error" && !state.fieldErrors ? state.message : undefined;
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<form action={formAction} noValidate>
|
|
102
|
-
{formError && (
|
|
103
|
-
<Alert variant="error" data-testid="contact-form-error">
|
|
104
|
-
<AlertDescription>{formError}</AlertDescription>
|
|
105
|
-
</Alert>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
{/* Honeypot — hidden from users; bots fill it; API rejects non-empty */}
|
|
109
|
-
<div hidden>
|
|
110
|
-
<Input
|
|
111
|
-
type="text"
|
|
112
|
-
name="website"
|
|
113
|
-
tabIndex={-1}
|
|
114
|
-
aria-hidden="true"
|
|
115
|
-
data-testid="contact-honeypot"
|
|
116
|
-
/>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div>
|
|
120
|
-
<Label htmlFor="contact-name">{t("nameLabel")}</Label>
|
|
121
|
-
<Input
|
|
122
|
-
id="contact-name"
|
|
123
|
-
name="name"
|
|
124
|
-
type="text"
|
|
125
|
-
autoComplete="name"
|
|
126
|
-
data-testid="contact-name-input"
|
|
127
|
-
required
|
|
128
|
-
defaultValue={initialValues?.name}
|
|
129
|
-
aria-describedby={fieldErrors.name ? "contact-name-error" : undefined}
|
|
130
|
-
/>
|
|
131
|
-
{fieldErrors.name && (
|
|
132
|
-
<InlineMessage
|
|
133
|
-
variant="error"
|
|
134
|
-
id="contact-name-error"
|
|
135
|
-
data-testid="contact-name-error"
|
|
136
|
-
>
|
|
137
|
-
{fieldErrors.name}
|
|
138
|
-
</InlineMessage>
|
|
139
|
-
)}
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<div>
|
|
143
|
-
<Label htmlFor="contact-email">{t("emailLabel")}</Label>
|
|
144
|
-
<Input
|
|
145
|
-
id="contact-email"
|
|
146
|
-
name="email"
|
|
147
|
-
type="email"
|
|
148
|
-
autoComplete="email"
|
|
149
|
-
data-testid="contact-email-input"
|
|
150
|
-
required
|
|
151
|
-
defaultValue={initialValues?.email}
|
|
152
|
-
aria-describedby={
|
|
153
|
-
fieldErrors.email ? "contact-email-error" : undefined
|
|
154
|
-
}
|
|
155
|
-
/>
|
|
156
|
-
{fieldErrors.email && (
|
|
157
|
-
<InlineMessage
|
|
158
|
-
variant="error"
|
|
159
|
-
id="contact-email-error"
|
|
160
|
-
data-testid="contact-email-error"
|
|
161
|
-
>
|
|
162
|
-
{fieldErrors.email}
|
|
163
|
-
</InlineMessage>
|
|
164
|
-
)}
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
<div>
|
|
168
|
-
<Label htmlFor="contact-message">{t("messageLabel")}</Label>
|
|
169
|
-
<Textarea
|
|
170
|
-
id="contact-message"
|
|
171
|
-
name="message"
|
|
172
|
-
rows={5}
|
|
173
|
-
data-testid="contact-message-input"
|
|
174
|
-
defaultValue={initialValues?.message}
|
|
175
|
-
aria-describedby={
|
|
176
|
-
fieldErrors.message ? "contact-message-error" : undefined
|
|
177
|
-
}
|
|
178
|
-
/>
|
|
179
|
-
{fieldErrors.message && (
|
|
180
|
-
<InlineMessage
|
|
181
|
-
variant="error"
|
|
182
|
-
id="contact-message-error"
|
|
183
|
-
data-testid="contact-message-error"
|
|
184
|
-
>
|
|
185
|
-
{fieldErrors.message}
|
|
186
|
-
</InlineMessage>
|
|
187
|
-
)}
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
<div className="flex items-start gap-3">
|
|
191
|
-
<Checkbox
|
|
192
|
-
id="contact-consent"
|
|
193
|
-
name="consent"
|
|
194
|
-
value="true"
|
|
195
|
-
checked={consented}
|
|
196
|
-
onCheckedChange={handleConsentChange}
|
|
197
|
-
aria-labelledby="contact-consent-statement"
|
|
198
|
-
className="mt-0.5"
|
|
199
|
-
data-testid="contact-consent"
|
|
200
|
-
/>
|
|
201
|
-
<ConsentStatement
|
|
202
|
-
id="contact-consent-statement"
|
|
203
|
-
statement={t("consentStatement")}
|
|
204
|
-
policies={{ terms: localeHref(locale, "/terms") }}
|
|
205
|
-
/>
|
|
206
|
-
</div>
|
|
207
|
-
|
|
208
|
-
<SubmitButton disabled={!consented} />
|
|
209
|
-
</form>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for ContactForm's consent row (#6466, spec AC-2 / AC-3).
|
|
3
|
-
*
|
|
4
|
-
* ContactForm is the only form a /contact visitor can submit — the fallback
|
|
5
|
-
* `ConversationalIntakeWrapper` swaps in on Skip or a failed auto-submit. Its
|
|
6
|
-
* consent line must be a real control: a visible, unticked checkbox named by
|
|
7
|
-
* a statement whose "Terms" is a link, and a submit button that stays
|
|
8
|
-
* disabled until the box is ticked (the form is `noValidate`, so `required`
|
|
9
|
-
* alone would gate nothing).
|
|
10
|
-
*
|
|
11
|
-
* jsdom lacks `useFormState`/`useFormStatus` (see
|
|
12
|
-
* page-renderer-migration.test.tsx), so `react-dom` is extended with inert
|
|
13
|
-
* stand-ins; the outermost network boundary (`@working-theory/api-client`)
|
|
14
|
-
* is mocked so the real server action module loads without a network.
|
|
15
|
-
*/
|
|
16
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
17
|
-
import { localeHref } from "@working-theory/ui";
|
|
18
|
-
|
|
19
|
-
const MESSAGES: Record<string, string> = {
|
|
20
|
-
consentStatement: "I accept the [Terms](terms).",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
jest.mock("next-intl", () => ({
|
|
24
|
-
useTranslations: () => (key: string) => MESSAGES[key] ?? key,
|
|
25
|
-
useLocale: () => "en",
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
jest.mock("react-dom", () => ({
|
|
29
|
-
...jest.requireActual("react-dom"),
|
|
30
|
-
useFormState: (_action: unknown, initial: unknown) => [initial, jest.fn()],
|
|
31
|
-
useFormStatus: () => ({ pending: false }),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
jest.mock("@working-theory/api-client", () => ({
|
|
35
|
-
leads: { submit: jest.fn() },
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
import { ContactForm } from "../ContactForm.js";
|
|
39
|
-
|
|
40
|
-
// jsdom does not implement ResizeObserver — the Radix Checkbox renders a
|
|
41
|
-
// hidden form-bubble input that observes size. Polyfill with a no-op.
|
|
42
|
-
beforeAll(() => {
|
|
43
|
-
global.ResizeObserver = class ResizeObserver {
|
|
44
|
-
observe() {}
|
|
45
|
-
unobserve() {}
|
|
46
|
-
disconnect() {}
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("ContactForm consent row (#6466)", () => {
|
|
51
|
-
it("renders a visible, unticked checkbox (AC-2 i)", () => {
|
|
52
|
-
render(<ContactForm />);
|
|
53
|
-
const checkbox = screen.getByRole("checkbox");
|
|
54
|
-
expect(checkbox).toBeVisible();
|
|
55
|
-
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("links 'Terms' to the locale-aware /terms route (AC-2 ii)", () => {
|
|
59
|
-
render(<ContactForm />);
|
|
60
|
-
const link = screen.getByRole("link", { name: "Terms" });
|
|
61
|
-
expect(link).toHaveAttribute("href", localeHref("en", "/terms"));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("names the checkbox by the consent statement (AC-2 iii)", () => {
|
|
65
|
-
render(<ContactForm />);
|
|
66
|
-
// jsdom's accname pads the inline link with a space ("Terms .").
|
|
67
|
-
expect(screen.getByRole("checkbox")).toHaveAccessibleName(
|
|
68
|
-
/^I accept the Terms ?\.$/,
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("clicking the Terms link does not tick the box (AC-2 iv)", () => {
|
|
73
|
-
render(<ContactForm />);
|
|
74
|
-
const link = screen.getByRole("link", { name: "Terms" });
|
|
75
|
-
// A wrapping <label> is what would forward the click to the box.
|
|
76
|
-
expect(link.closest("label")).toBeNull();
|
|
77
|
-
link.addEventListener("click", (event) => event.preventDefault());
|
|
78
|
-
fireEvent.click(link);
|
|
79
|
-
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
80
|
-
"aria-checked",
|
|
81
|
-
"false",
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("keeps submit disabled until the box is ticked (AC-3)", () => {
|
|
86
|
-
render(<ContactForm />);
|
|
87
|
-
const submit = screen.getByTestId("contact-submit");
|
|
88
|
-
expect(submit).toBeDisabled();
|
|
89
|
-
fireEvent.click(screen.getByRole("checkbox"));
|
|
90
|
-
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
91
|
-
"aria-checked",
|
|
92
|
-
"true",
|
|
93
|
-
);
|
|
94
|
-
expect(submit).toBeEnabled();
|
|
95
|
-
});
|
|
96
|
-
});
|