create-nextdevtpl 2.6.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 (425) hide show
  1. package/README.md +29 -0
  2. package/catalog.json +385 -0
  3. package/dist/catalog.d.ts +3 -0
  4. package/dist/catalog.js +112 -0
  5. package/dist/catalog.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.js +115 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/filesystem.d.ts +10 -0
  10. package/dist/filesystem.js +86 -0
  11. package/dist/filesystem.js.map +1 -0
  12. package/dist/generate.d.ts +5 -0
  13. package/dist/generate.js +758 -0
  14. package/dist/generate.js.map +1 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/selection.d.ts +2 -0
  19. package/dist/selection.js +111 -0
  20. package/dist/selection.js.map +1 -0
  21. package/dist/service-templates.d.ts +4 -0
  22. package/dist/service-templates.js +180 -0
  23. package/dist/service-templates.js.map +1 -0
  24. package/dist/types.d.ts +65 -0
  25. package/dist/types.js +2 -0
  26. package/dist/types.js.map +1 -0
  27. package/package.json +54 -0
  28. package/template/.dockerignore +14 -0
  29. package/template/.env.example +282 -0
  30. package/template/.gitattributes +17 -0
  31. package/template/.node-version +1 -0
  32. package/template/Dockerfile +45 -0
  33. package/template/LICENSE +21 -0
  34. package/template/README.md +267 -0
  35. package/template/biome.json +68 -0
  36. package/template/cloudflare/templates/logger.ts +162 -0
  37. package/template/cloudflare/templates/mail/server.ts +6 -0
  38. package/template/cloudflare/templates/mail/templates.ts +87 -0
  39. package/template/cloudflare/templates/mail/utils.ts +69 -0
  40. package/template/cloudflare/worker.mjs +25 -0
  41. package/template/components.json +22 -0
  42. package/template/compose.yaml +74 -0
  43. package/template/deploy/check-health.mjs +25 -0
  44. package/template/deploy/docker/README.md +14 -0
  45. package/template/deploy/server/README.md +24 -0
  46. package/template/deploy/server/build.sh +24 -0
  47. package/template/deploy/server/nextdevtpl.service +19 -0
  48. package/template/deploy/server/start.sh +19 -0
  49. package/template/deploy/vercel/README.md +16 -0
  50. package/template/deploy/verify-production-health.mjs +86 -0
  51. package/template/drizzle.config.ts +47 -0
  52. package/template/gitignore +62 -0
  53. package/template/manifest.json +35 -0
  54. package/template/messages/en.json +766 -0
  55. package/template/messages/zh.json +762 -0
  56. package/template/next.config.mjs +28 -0
  57. package/template/open-next.config.ts +3 -0
  58. package/template/package.json +125 -0
  59. package/template/pnpm-lock.yaml +15901 -0
  60. package/template/postcss.config.mjs +7 -0
  61. package/template/public/file.svg +1 -0
  62. package/template/public/globe.svg +1 -0
  63. package/template/public/logo.svg +6 -0
  64. package/template/public/next.svg +1 -0
  65. package/template/public/vercel.svg +1 -0
  66. package/template/public/window.svg +1 -0
  67. package/template/sentry.client.config.ts +10 -0
  68. package/template/sentry.edge.config.ts +10 -0
  69. package/template/sentry.server.config.ts +10 -0
  70. package/template/source.config.ts +59 -0
  71. package/template/src/adapters/ai/anthropic.ts +113 -0
  72. package/template/src/adapters/ai/index.ts +6 -0
  73. package/template/src/adapters/ai/openai-compatible.ts +88 -0
  74. package/template/src/adapters/ai/workers-ai.ts +63 -0
  75. package/template/src/adapters/index.ts +10 -0
  76. package/template/src/adapters/jobs/cloudflare-workflows.ts +46 -0
  77. package/template/src/adapters/jobs/index.ts +5 -0
  78. package/template/src/adapters/jobs/inngest/adapter.ts +29 -0
  79. package/template/src/adapters/jobs/inngest/client.ts +8 -0
  80. package/template/src/adapters/jobs/inngest/functions.ts +17 -0
  81. package/template/src/adapters/jobs/inngest/handler.ts +6 -0
  82. package/template/src/adapters/jobs/inngest/index.ts +4 -0
  83. package/template/src/adapters/mail/cloudflare-email.ts +50 -0
  84. package/template/src/adapters/mail/disabled.ts +17 -0
  85. package/template/src/adapters/mail/index.ts +7 -0
  86. package/template/src/adapters/mail/resend.ts +76 -0
  87. package/template/src/adapters/mail/smtp.ts +82 -0
  88. package/template/src/adapters/payment/creem.ts +269 -0
  89. package/template/src/adapters/payment/index.ts +2 -0
  90. package/template/src/adapters/payment/stripe.ts +302 -0
  91. package/template/src/adapters/rate-limit/cloudflare.ts +42 -0
  92. package/template/src/adapters/rate-limit/index.ts +6 -0
  93. package/template/src/adapters/rate-limit/noop.ts +27 -0
  94. package/template/src/adapters/rate-limit/upstash.ts +113 -0
  95. package/template/src/adapters/registry.ts +228 -0
  96. package/template/src/adapters/storage/index.ts +3 -0
  97. package/template/src/adapters/storage/r2-binding.ts +105 -0
  98. package/template/src/adapters/storage/s3-compatible.ts +149 -0
  99. package/template/src/app/[locale]/(admin)/admin/layout.tsx +36 -0
  100. package/template/src/app/[locale]/(admin)/admin/page.tsx +345 -0
  101. package/template/src/app/[locale]/(admin)/admin/tickets/[id]/page.tsx +261 -0
  102. package/template/src/app/[locale]/(admin)/admin/tickets/page.tsx +236 -0
  103. package/template/src/app/[locale]/(admin)/admin/users/page.tsx +557 -0
  104. package/template/src/app/[locale]/(auth)/forgot-password/page.tsx +9 -0
  105. package/template/src/app/[locale]/(auth)/layout.tsx +28 -0
  106. package/template/src/app/[locale]/(auth)/sign-in/page.tsx +9 -0
  107. package/template/src/app/[locale]/(auth)/sign-up/page.tsx +9 -0
  108. package/template/src/app/[locale]/(dashboard)/dashboard/credits/buy/buy-credits-view.tsx +170 -0
  109. package/template/src/app/[locale]/(dashboard)/dashboard/credits/buy/page.tsx +12 -0
  110. package/template/src/app/[locale]/(dashboard)/dashboard/credits/page.tsx +5 -0
  111. package/template/src/app/[locale]/(dashboard)/dashboard/loading.tsx +41 -0
  112. package/template/src/app/[locale]/(dashboard)/dashboard/page.tsx +299 -0
  113. package/template/src/app/[locale]/(dashboard)/dashboard/settings/page.tsx +38 -0
  114. package/template/src/app/[locale]/(dashboard)/dashboard/support/[id]/page.tsx +220 -0
  115. package/template/src/app/[locale]/(dashboard)/dashboard/support/new/page.tsx +183 -0
  116. package/template/src/app/[locale]/(dashboard)/dashboard/support/page.tsx +145 -0
  117. package/template/src/app/[locale]/(dashboard)/layout.tsx +35 -0
  118. package/template/src/app/[locale]/(marketing)/blog/[slug]/page.tsx +178 -0
  119. package/template/src/app/[locale]/(marketing)/blog/blog-post-card.tsx +95 -0
  120. package/template/src/app/[locale]/(marketing)/blog/page.tsx +127 -0
  121. package/template/src/app/[locale]/(marketing)/demo/plan-badges/page.tsx +36 -0
  122. package/template/src/app/[locale]/(marketing)/layout.tsx +15 -0
  123. package/template/src/app/[locale]/(marketing)/legal/[slug]/page.tsx +130 -0
  124. package/template/src/app/[locale]/(marketing)/page.tsx +94 -0
  125. package/template/src/app/[locale]/(marketing)/pseo/[slug]/page.tsx +68 -0
  126. package/template/src/app/[locale]/(marketing)/pseo/page.tsx +137 -0
  127. package/template/src/app/[locale]/docs/[[...slug]]/page.tsx +63 -0
  128. package/template/src/app/[locale]/docs/layout.tsx +39 -0
  129. package/template/src/app/[locale]/layout.tsx +80 -0
  130. package/template/src/app/[locale]/opengraph-image.tsx +20 -0
  131. package/template/src/app/[locale]/twitter-image.tsx +20 -0
  132. package/template/src/app/api/auth/[...all]/route.ts +20 -0
  133. package/template/src/app/api/health/route.ts +45 -0
  134. package/template/src/app/api/inngest/route.ts +12 -0
  135. package/template/src/app/api/jobs/credits/expire/route.ts +102 -0
  136. package/template/src/app/api/search/route.ts +17 -0
  137. package/template/src/app/api/upload/presigned/route.ts +98 -0
  138. package/template/src/app/api/webhooks/payment/route.ts +363 -0
  139. package/template/src/app/globals.css +406 -0
  140. package/template/src/app/icon.svg +6 -0
  141. package/template/src/app/image-proxy/[...path]/route.ts +137 -0
  142. package/template/src/app/layout.tsx +73 -0
  143. package/template/src/app/robots.ts +29 -0
  144. package/template/src/app/sitemap.ts +68 -0
  145. package/template/src/components/motion/reveal.tsx +49 -0
  146. package/template/src/components/seo/json-ld.tsx +87 -0
  147. package/template/src/components/seo/og-image-template.tsx +118 -0
  148. package/template/src/components/ui/accordion.tsx +66 -0
  149. package/template/src/components/ui/alert-dialog.tsx +195 -0
  150. package/template/src/components/ui/avatar.tsx +53 -0
  151. package/template/src/components/ui/badge.tsx +46 -0
  152. package/template/src/components/ui/button.tsx +65 -0
  153. package/template/src/components/ui/card.tsx +92 -0
  154. package/template/src/components/ui/checkbox.tsx +32 -0
  155. package/template/src/components/ui/collapsible.tsx +33 -0
  156. package/template/src/components/ui/dialog.tsx +124 -0
  157. package/template/src/components/ui/dropdown-menu.tsx +257 -0
  158. package/template/src/components/ui/form.tsx +233 -0
  159. package/template/src/components/ui/index.ts +2 -0
  160. package/template/src/components/ui/input.tsx +21 -0
  161. package/template/src/components/ui/label.tsx +24 -0
  162. package/template/src/components/ui/navigation-menu.tsx +167 -0
  163. package/template/src/components/ui/pagination.tsx +125 -0
  164. package/template/src/components/ui/popover.tsx +48 -0
  165. package/template/src/components/ui/progress.tsx +27 -0
  166. package/template/src/components/ui/radio-group.tsx +45 -0
  167. package/template/src/components/ui/select.tsx +190 -0
  168. package/template/src/components/ui/separator.tsx +28 -0
  169. package/template/src/components/ui/sheet.tsx +140 -0
  170. package/template/src/components/ui/switch.tsx +31 -0
  171. package/template/src/components/ui/tabs.tsx +75 -0
  172. package/template/src/components/ui/textarea.tsx +18 -0
  173. package/template/src/config/index.ts +31 -0
  174. package/template/src/config/nav.ts +238 -0
  175. package/template/src/config/payment.ts +282 -0
  176. package/template/src/config/site.ts +53 -0
  177. package/template/src/config/subscription-plan.ts +164 -0
  178. package/template/src/content/blog/en/hello-world.mdx +48 -0
  179. package/template/src/content/blog/zh/hello-world.mdx +48 -0
  180. package/template/src/content/blog/zh/nextdevtpl-linuxdo-intro.mdx +98 -0
  181. package/template/src/content/docs/en/admin-and-support.mdx +65 -0
  182. package/template/src/content/docs/en/authentication.mdx +71 -0
  183. package/template/src/content/docs/en/background-jobs.mdx +53 -0
  184. package/template/src/content/docs/en/configuration.mdx +71 -0
  185. package/template/src/content/docs/en/credits.mdx +69 -0
  186. package/template/src/content/docs/en/customization.mdx +95 -0
  187. package/template/src/content/docs/en/database.mdx +66 -0
  188. package/template/src/content/docs/en/deployment.mdx +62 -0
  189. package/template/src/content/docs/en/email.mdx +48 -0
  190. package/template/src/content/docs/en/index.mdx +48 -0
  191. package/template/src/content/docs/en/internationalization.mdx +78 -0
  192. package/template/src/content/docs/en/meta.json +22 -0
  193. package/template/src/content/docs/en/observability.mdx +69 -0
  194. package/template/src/content/docs/en/payments.mdx +64 -0
  195. package/template/src/content/docs/en/project-structure.mdx +82 -0
  196. package/template/src/content/docs/en/quick-start.mdx +113 -0
  197. package/template/src/content/docs/en/storage.mdx +51 -0
  198. package/template/src/content/docs/zh/admin-and-support.mdx +65 -0
  199. package/template/src/content/docs/zh/authentication.mdx +71 -0
  200. package/template/src/content/docs/zh/background-jobs.mdx +53 -0
  201. package/template/src/content/docs/zh/configuration.mdx +70 -0
  202. package/template/src/content/docs/zh/credits.mdx +69 -0
  203. package/template/src/content/docs/zh/customization.mdx +95 -0
  204. package/template/src/content/docs/zh/database.mdx +66 -0
  205. package/template/src/content/docs/zh/deployment.mdx +61 -0
  206. package/template/src/content/docs/zh/email.mdx +47 -0
  207. package/template/src/content/docs/zh/index.mdx +48 -0
  208. package/template/src/content/docs/zh/internationalization.mdx +78 -0
  209. package/template/src/content/docs/zh/meta.json +22 -0
  210. package/template/src/content/docs/zh/observability.mdx +68 -0
  211. package/template/src/content/docs/zh/payments.mdx +64 -0
  212. package/template/src/content/docs/zh/project-structure.mdx +82 -0
  213. package/template/src/content/docs/zh/quick-start.mdx +110 -0
  214. package/template/src/content/docs/zh/storage.mdx +51 -0
  215. package/template/src/content/legal/en/cookie-policy.mdx +78 -0
  216. package/template/src/content/legal/en/privacy.mdx +131 -0
  217. package/template/src/content/legal/en/terms.mdx +93 -0
  218. package/template/src/content/legal/zh/cookie-policy.mdx +78 -0
  219. package/template/src/content/legal/zh/privacy.mdx +131 -0
  220. package/template/src/content/legal/zh/terms.mdx +92 -0
  221. package/template/src/core/modules/contract.ts +225 -0
  222. package/template/src/core/modules/index.ts +1 -0
  223. package/template/src/core/services/ai.ts +31 -0
  224. package/template/src/core/services/common.ts +112 -0
  225. package/template/src/core/services/index.ts +8 -0
  226. package/template/src/core/services/jobs.ts +24 -0
  227. package/template/src/core/services/mail.ts +35 -0
  228. package/template/src/core/services/manifest.ts +51 -0
  229. package/template/src/core/services/payment.ts +92 -0
  230. package/template/src/core/services/rate-limit.ts +47 -0
  231. package/template/src/core/services/storage.ts +42 -0
  232. package/template/src/db/index.ts +71 -0
  233. package/template/src/db/schema/auth.ts +67 -0
  234. package/template/src/db/schema/credits.ts +94 -0
  235. package/template/src/db/schema/index.ts +5 -0
  236. package/template/src/db/schema/mail.ts +14 -0
  237. package/template/src/db/schema/subscription.ts +21 -0
  238. package/template/src/db/schema/support.ts +56 -0
  239. package/template/src/db/schema.ts +2 -0
  240. package/template/src/features/admin/components/admin-sidebar.tsx +213 -0
  241. package/template/src/features/admin/components/index.ts +2 -0
  242. package/template/src/features/admin/index.ts +3 -0
  243. package/template/src/features/admin/manifest.ts +41 -0
  244. package/template/src/features/analytics/components/analytics.tsx +62 -0
  245. package/template/src/features/analytics/components/index.ts +2 -0
  246. package/template/src/features/analytics/index.ts +3 -0
  247. package/template/src/features/analytics/manifest.ts +13 -0
  248. package/template/src/features/auth/components/auth-brand-panel.tsx +82 -0
  249. package/template/src/features/auth/components/auth-error-alert.tsx +28 -0
  250. package/template/src/features/auth/components/auth-footer.tsx +52 -0
  251. package/template/src/features/auth/components/auth-logo.tsx +30 -0
  252. package/template/src/features/auth/components/forgot-password-form.tsx +146 -0
  253. package/template/src/features/auth/components/index.ts +9 -0
  254. package/template/src/features/auth/components/sign-in-form.tsx +241 -0
  255. package/template/src/features/auth/components/sign-up-form.tsx +307 -0
  256. package/template/src/features/auth/index.ts +3 -0
  257. package/template/src/features/auth/manifest.ts +30 -0
  258. package/template/src/features/blog/components/blog-post-item.tsx +72 -0
  259. package/template/src/features/blog/components/index.ts +1 -0
  260. package/template/src/features/blog/data/mock-posts.ts +62 -0
  261. package/template/src/features/blog/index.ts +3 -0
  262. package/template/src/features/blog/manifest.ts +22 -0
  263. package/template/src/features/credits/account.ts +76 -0
  264. package/template/src/features/credits/actions.ts +427 -0
  265. package/template/src/features/credits/components/credit-balance-badge.tsx +54 -0
  266. package/template/src/features/credits/components/credit-usage-section.tsx +160 -0
  267. package/template/src/features/credits/components/index.ts +7 -0
  268. package/template/src/features/credits/components/transaction-history.tsx +346 -0
  269. package/template/src/features/credits/config.ts +70 -0
  270. package/template/src/features/credits/consume.ts +140 -0
  271. package/template/src/features/credits/core.ts +23 -0
  272. package/template/src/features/credits/errors.ts +26 -0
  273. package/template/src/features/credits/expire.ts +112 -0
  274. package/template/src/features/credits/grant.ts +168 -0
  275. package/template/src/features/credits/index.ts +6 -0
  276. package/template/src/features/credits/manifest.ts +36 -0
  277. package/template/src/features/credits/query.ts +65 -0
  278. package/template/src/features/credits/types.ts +48 -0
  279. package/template/src/features/dashboard/components/index.ts +3 -0
  280. package/template/src/features/dashboard/components/main-wrapper.tsx +100 -0
  281. package/template/src/features/dashboard/components/sidebar.tsx +341 -0
  282. package/template/src/features/dashboard/context/index.ts +2 -0
  283. package/template/src/features/dashboard/context/sidebar-context.tsx +72 -0
  284. package/template/src/features/dashboard/index.ts +4 -0
  285. package/template/src/features/dashboard/manifest.ts +28 -0
  286. package/template/src/features/mail/actions.ts +175 -0
  287. package/template/src/features/mail/index.ts +5 -0
  288. package/template/src/features/mail/manifest.ts +13 -0
  289. package/template/src/features/mail/server.ts +8 -0
  290. package/template/src/features/mail/templates/index.ts +11 -0
  291. package/template/src/features/mail/templates/primary-action-email.tsx +224 -0
  292. package/template/src/features/mail/templates/welcome-email.tsx +121 -0
  293. package/template/src/features/mail/utils.ts +190 -0
  294. package/template/src/features/marketing/components/animated-price.tsx +51 -0
  295. package/template/src/features/marketing/components/cookie-consent.tsx +255 -0
  296. package/template/src/features/marketing/components/cta-section.tsx +50 -0
  297. package/template/src/features/marketing/components/faq-section.tsx +59 -0
  298. package/template/src/features/marketing/components/feature-grid.tsx +80 -0
  299. package/template/src/features/marketing/components/footer.tsx +128 -0
  300. package/template/src/features/marketing/components/header.tsx +255 -0
  301. package/template/src/features/marketing/components/hero-section.tsx +323 -0
  302. package/template/src/features/marketing/components/how-it-works.tsx +115 -0
  303. package/template/src/features/marketing/components/index.ts +14 -0
  304. package/template/src/features/marketing/components/nav-menu.tsx +236 -0
  305. package/template/src/features/marketing/components/pricing-section.tsx +446 -0
  306. package/template/src/features/marketing/components/testimonials.tsx +59 -0
  307. package/template/src/features/marketing/components/use-cases-section.tsx +78 -0
  308. package/template/src/features/marketing/index.ts +3 -0
  309. package/template/src/features/marketing/manifest.ts +35 -0
  310. package/template/src/features/payment/actions.ts +182 -0
  311. package/template/src/features/payment/index.ts +4 -0
  312. package/template/src/features/payment/manifest.ts +18 -0
  313. package/template/src/features/payment/server.ts +7 -0
  314. package/template/src/features/payment/types.ts +202 -0
  315. package/template/src/features/pseo/components/index.ts +6 -0
  316. package/template/src/features/pseo/components/pseo-cta.tsx +51 -0
  317. package/template/src/features/pseo/components/pseo-faq.tsx +40 -0
  318. package/template/src/features/pseo/components/pseo-feature-grid.tsx +74 -0
  319. package/template/src/features/pseo/components/pseo-hero.tsx +106 -0
  320. package/template/src/features/pseo/components/pseo-related.tsx +62 -0
  321. package/template/src/features/pseo/components/pseo-use-cases.tsx +50 -0
  322. package/template/src/features/pseo/data/pseo-pages.json +534 -0
  323. package/template/src/features/pseo/index.ts +4 -0
  324. package/template/src/features/pseo/lib/pseo-data.ts +140 -0
  325. package/template/src/features/pseo/manifest.ts +22 -0
  326. package/template/src/features/settings/actions/delete-account.ts +50 -0
  327. package/template/src/features/settings/actions/index.ts +3 -0
  328. package/template/src/features/settings/actions/update-profile.ts +49 -0
  329. package/template/src/features/settings/components/billing-section.tsx +283 -0
  330. package/template/src/features/settings/components/index.ts +3 -0
  331. package/template/src/features/settings/components/profile-form.tsx +154 -0
  332. package/template/src/features/settings/components/security-section.tsx +158 -0
  333. package/template/src/features/settings/components/settings-profile-view.tsx +506 -0
  334. package/template/src/features/settings/index.ts +5 -0
  335. package/template/src/features/settings/manifest.ts +28 -0
  336. package/template/src/features/settings/schemas/index.ts +2 -0
  337. package/template/src/features/settings/schemas/update-profile.ts +21 -0
  338. package/template/src/features/shared/components/cookie-settings-dialog.tsx +282 -0
  339. package/template/src/features/shared/components/index.ts +5 -0
  340. package/template/src/features/shared/components/language-switcher.tsx +82 -0
  341. package/template/src/features/shared/components/max-width-wrapper.tsx +49 -0
  342. package/template/src/features/shared/components/mode-toggle.tsx +119 -0
  343. package/template/src/features/shared/icons/google-icon.tsx +39 -0
  344. package/template/src/features/shared/icons/index.ts +7 -0
  345. package/template/src/features/shared/index.ts +5 -0
  346. package/template/src/features/shared/manifest.ts +13 -0
  347. package/template/src/features/shared/providers.tsx +59 -0
  348. package/template/src/features/storage/actions.ts +107 -0
  349. package/template/src/features/storage/index.ts +6 -0
  350. package/template/src/features/storage/manifest.ts +22 -0
  351. package/template/src/features/storage/types.ts +143 -0
  352. package/template/src/features/storage/utils.ts +85 -0
  353. package/template/src/features/storage/validation.ts +151 -0
  354. package/template/src/features/subscription/actions/get-user-plan.ts +26 -0
  355. package/template/src/features/subscription/actions/index.ts +5 -0
  356. package/template/src/features/subscription/components/current-plan-badge.tsx +18 -0
  357. package/template/src/features/subscription/components/index.ts +6 -0
  358. package/template/src/features/subscription/components/plan-badge.tsx +98 -0
  359. package/template/src/features/subscription/index.ts +25 -0
  360. package/template/src/features/subscription/manifest.ts +13 -0
  361. package/template/src/features/subscription/services/user-plan.ts +181 -0
  362. package/template/src/features/support/actions/admin-ticket.ts +172 -0
  363. package/template/src/features/support/actions/admin-users.ts +295 -0
  364. package/template/src/features/support/actions/index.ts +22 -0
  365. package/template/src/features/support/actions/ticket.ts +12 -0
  366. package/template/src/features/support/actions/user-ticket.ts +153 -0
  367. package/template/src/features/support/components/admin-ticket-reply-form.tsx +109 -0
  368. package/template/src/features/support/components/admin-ticket-status-select.tsx +119 -0
  369. package/template/src/features/support/components/index.ts +6 -0
  370. package/template/src/features/support/components/ticket-message-form.tsx +99 -0
  371. package/template/src/features/support/components/user-role-select.tsx +107 -0
  372. package/template/src/features/support/index.ts +5 -0
  373. package/template/src/features/support/manifest.ts +53 -0
  374. package/template/src/features/support/schemas/index.ts +15 -0
  375. package/template/src/features/support/schemas/ticket.ts +81 -0
  376. package/template/src/hooks/index.ts +2 -0
  377. package/template/src/i18n/index.ts +12 -0
  378. package/template/src/i18n/request.ts +24 -0
  379. package/template/src/i18n/routing.ts +23 -0
  380. package/template/src/instrumentation.ts +19 -0
  381. package/template/src/lib/ai/index.ts +16 -0
  382. package/template/src/lib/api-logger.ts +24 -0
  383. package/template/src/lib/auth/admin.ts +54 -0
  384. package/template/src/lib/auth/api.ts +17 -0
  385. package/template/src/lib/auth/client.ts +227 -0
  386. package/template/src/lib/auth/edge.ts +84 -0
  387. package/template/src/lib/auth/errors.ts +99 -0
  388. package/template/src/lib/auth/index.ts +159 -0
  389. package/template/src/lib/auth/server.ts +46 -0
  390. package/template/src/lib/auth/session-context.tsx +96 -0
  391. package/template/src/lib/cloudflare/bindings.ts +31 -0
  392. package/template/src/lib/cookie-consent.ts +33 -0
  393. package/template/src/lib/file-utils.ts +45 -0
  394. package/template/src/lib/fingerprint/index.ts +73 -0
  395. package/template/src/lib/health/checks.ts +6 -0
  396. package/template/src/lib/health/core.ts +86 -0
  397. package/template/src/lib/health/index.ts +11 -0
  398. package/template/src/lib/health/types.ts +13 -0
  399. package/template/src/lib/index.ts +2 -0
  400. package/template/src/lib/logger/index.ts +258 -0
  401. package/template/src/lib/monitoring/index.ts +384 -0
  402. package/template/src/lib/rate-limit/anonymous.ts +85 -0
  403. package/template/src/lib/rate-limit/index.ts +111 -0
  404. package/template/src/lib/safe-action.ts +127 -0
  405. package/template/src/lib/seo/json-ld.ts +188 -0
  406. package/template/src/lib/source.ts +122 -0
  407. package/template/src/lib/utils.ts +6 -0
  408. package/template/src/middleware.ts +162 -0
  409. package/template/src/modules/index.ts +10 -0
  410. package/template/src/modules/presets.ts +8 -0
  411. package/template/src/modules/registry.ts +34 -0
  412. package/template/src/modules/selection.ts +45 -0
  413. package/template/src/services/ai.ts +49 -0
  414. package/template/src/services/index.ts +6 -0
  415. package/template/src/services/jobs.ts +3 -0
  416. package/template/src/services/mail.ts +7 -0
  417. package/template/src/services/payment.ts +3 -0
  418. package/template/src/services/rate-limit.ts +12 -0
  419. package/template/src/services/storage.ts +3 -0
  420. package/template/src/types/fumadocs-source.d.ts +14 -0
  421. package/template/src/types/index.ts +2 -0
  422. package/template/tsconfig.json +53 -0
  423. package/template/vercel.json +11 -0
  424. package/template/vitest.config.ts +51 -0
  425. package/template/wrangler.jsonc +30 -0
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { Quote } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+
6
+ import { Reveal } from "@/components/motion/reveal";
7
+
8
+ export function Testimonials() {
9
+ const t = useTranslations("Testimonials");
10
+
11
+ // 取前 3 条,保持页面紧凑
12
+ const testimonialItems = [0, 1, 2].map((i) => ({
13
+ content: t(`items.${i}.content`),
14
+ author: t(`items.${i}.author`),
15
+ role: t(`items.${i}.role`),
16
+ }));
17
+
18
+ return (
19
+ <section className="border-t py-24">
20
+ <div className="container">
21
+ <Reveal>
22
+ <div className="mb-14 max-w-2xl">
23
+ <span className="eyebrow">{t("label")}</span>
24
+ <h2 className="mt-4 text-balance text-3xl font-extrabold tracking-tight text-foreground md:text-4xl">
25
+ {t("title")}
26
+ </h2>
27
+ <p className="mt-4 text-muted-foreground">{t("subtitle")}</p>
28
+ </div>
29
+ </Reveal>
30
+
31
+ <Reveal delay={0.08}>
32
+ <div className="grid gap-px overflow-hidden rounded-xl border bg-border md:grid-cols-3">
33
+ {testimonialItems.map((testimonial) => (
34
+ <figure
35
+ key={testimonial.author}
36
+ className="flex flex-col justify-between gap-6 bg-card p-7"
37
+ >
38
+ <blockquote>
39
+ <Quote className="mb-3 h-5 w-5 text-primary" />
40
+ <p className="text-sm leading-relaxed text-foreground/80">
41
+ {testimonial.content}
42
+ </p>
43
+ </blockquote>
44
+ <figcaption className="border-t pt-4">
45
+ <div className="text-sm font-semibold text-foreground">
46
+ {testimonial.author}
47
+ </div>
48
+ <div className="mt-0.5 font-mono text-xs text-muted-foreground">
49
+ {testimonial.role}
50
+ </div>
51
+ </figcaption>
52
+ </figure>
53
+ ))}
54
+ </div>
55
+ </Reveal>
56
+ </div>
57
+ </section>
58
+ );
59
+ }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { BookOpen, Briefcase, GraduationCap, Languages } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { Card, CardContent } from "@/components/ui/card";
6
+
7
+ const useCaseConfig = [
8
+ { key: "students" as const, icon: GraduationCap },
9
+ { key: "professionals" as const, icon: Briefcase },
10
+ { key: "language" as const, icon: Languages },
11
+ { key: "knowledge" as const, icon: BookOpen },
12
+ ];
13
+
14
+ export function UseCasesSection() {
15
+ const t = useTranslations("UseCases");
16
+
17
+ return (
18
+ <section id="use-cases" className="container py-24">
19
+ <div className="mx-auto max-w-6xl">
20
+ {/* Header */}
21
+ <div className="mb-16 text-center">
22
+ <p className="mb-2 text-sm font-medium uppercase tracking-wider text-primary">
23
+ {t("label")}
24
+ </p>
25
+ <h2 className="mb-4 text-balance text-3xl font-bold tracking-tight md:text-4xl">
26
+ {t("title")}
27
+ </h2>
28
+ <p className="mx-auto max-w-2xl text-muted-foreground">
29
+ {t("subtitle")}
30
+ </p>
31
+ </div>
32
+
33
+ {/* Use Cases Grid */}
34
+ <div className="grid gap-6 md:grid-cols-2">
35
+ {useCaseConfig.map((uc) => {
36
+ const Icon = uc.icon;
37
+ const examples = t.raw(`items.${uc.key}.examples`) as string[];
38
+ return (
39
+ <Card
40
+ key={uc.key}
41
+ className="group border-border shadow-none transition-colors hover:border-primary/20"
42
+ >
43
+ <CardContent className="p-6">
44
+ <div className="flex items-start gap-4">
45
+ <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
46
+ <Icon className="h-6 w-6" />
47
+ </div>
48
+ <div className="flex-1">
49
+ <h3 className="font-semibold">
50
+ {t(`items.${uc.key}.title`)}
51
+ </h3>
52
+ <p className="text-sm text-primary">
53
+ {t(`items.${uc.key}.subtitle`)}
54
+ </p>
55
+ </div>
56
+ </div>
57
+ <p className="mt-4 text-sm text-muted-foreground">
58
+ {t(`items.${uc.key}.description`)}
59
+ </p>
60
+ <div className="mt-4 flex flex-wrap gap-2">
61
+ {examples.map((example) => (
62
+ <span
63
+ key={example}
64
+ className="inline-flex rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground"
65
+ >
66
+ {example}
67
+ </span>
68
+ ))}
69
+ </div>
70
+ </CardContent>
71
+ </Card>
72
+ );
73
+ })}
74
+ </div>
75
+ </div>
76
+ </section>
77
+ );
78
+ }
@@ -0,0 +1,3 @@
1
+ // Marketing Feature
2
+ export * from "./components";
3
+ export { marketingModule } from "./manifest";
@@ -0,0 +1,35 @@
1
+ import { defineModule } from "@/core/modules";
2
+
3
+ export const marketingModule = defineModule({
4
+ id: "marketing",
5
+ name: "Marketing",
6
+ description: "首页、定价、页头页脚与营销内容",
7
+ kind: "optional",
8
+ dependencies: ["payment", "shared"],
9
+ routes: [
10
+ {
11
+ path: "/",
12
+ source: "src/app/[locale]/(marketing)/page.tsx",
13
+ },
14
+ {
15
+ path: "/legal/[slug]",
16
+ source: "src/app/[locale]/(marketing)/legal/[slug]/page.tsx",
17
+ },
18
+ ],
19
+ navigation: [],
20
+ translations: [
21
+ "HomePage",
22
+ "Hero",
23
+ "Features",
24
+ "HowItWorks",
25
+ "UseCases",
26
+ "Testimonials",
27
+ "FAQ",
28
+ "CTA",
29
+ "Header",
30
+ "Navigation",
31
+ "Pricing",
32
+ "Footer",
33
+ ],
34
+ schema: [],
35
+ });
@@ -0,0 +1,182 @@
1
+ "use server";
2
+
3
+ import { eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { findPlanByPriceId, getBaseUrl, paymentConfig } from "@/config/payment";
7
+ import { db } from "@/db";
8
+ import { subscription } from "@/db/schema/subscription";
9
+ import { logEvent } from "@/lib/logger";
10
+ import { protectedAction } from "@/lib/safe-action";
11
+ import { paymentService } from "@/services/payment";
12
+
13
+ import { PaymentType } from "./types";
14
+
15
+ /**
16
+ * 创建支付 Checkout Session
17
+ *
18
+ * 支持订阅支付和一次性支付两种模式
19
+ */
20
+ export const createCheckoutSession = protectedAction
21
+ .metadata({ action: "payment.createCheckoutSession" })
22
+ .schema(
23
+ z.object({
24
+ priceId: z.string().min(1, "价格 ID 不能为空"),
25
+ type: z.nativeEnum(PaymentType).optional(),
26
+ successUrl: z.string().optional(),
27
+ cancelUrl: z.string().optional(),
28
+ })
29
+ )
30
+ .action(async ({ parsedInput, ctx }) => {
31
+ const { cancelUrl, priceId, successUrl, type } = parsedInput;
32
+ const { userId } = ctx;
33
+
34
+ // 检查是否已有活跃订阅
35
+ const [existingSub] = await db
36
+ .select({ status: subscription.status })
37
+ .from(subscription)
38
+ .where(eq(subscription.userId, userId))
39
+ .limit(1);
40
+
41
+ if (
42
+ existingSub &&
43
+ ["active", "trialing", "lifetime"].includes(existingSub.status)
44
+ ) {
45
+ throw new Error("您已有活跃订阅,请先取消当前订阅后再订阅新计划");
46
+ }
47
+
48
+ // 查找计划和价格信息
49
+ const { plan } = findPlanByPriceId(priceId);
50
+
51
+ const baseUrl = getBaseUrl();
52
+
53
+ logEvent("payment.checkout.started", {
54
+ userId,
55
+ priceId,
56
+ planId: plan?.id ?? "unknown",
57
+ provider: paymentService.provider,
58
+ });
59
+
60
+ const checkout = await paymentService.createCheckout({
61
+ productId: priceId,
62
+ mode: type === PaymentType.ONE_TIME ? "one-time" : "subscription",
63
+ successUrl:
64
+ successUrl ??
65
+ `${baseUrl}${paymentConfig.redirectAfterCheckout}?success=true`,
66
+ cancelUrl: cancelUrl ?? `${baseUrl}${paymentConfig.redirectAfterCancel}`,
67
+ requestId: `${userId}_${Date.now()}`,
68
+ metadata: {
69
+ userId,
70
+ planId: plan?.id ?? "unknown",
71
+ },
72
+ });
73
+
74
+ return { url: checkout.url };
75
+ });
76
+
77
+ /**
78
+ * 取消订阅
79
+ *
80
+ * 调用支付服务取消用户的订阅
81
+ */
82
+ export const cancelSubscription = protectedAction
83
+ .metadata({ action: "payment.cancelSubscription" })
84
+ .action(async ({ ctx }) => {
85
+ const { userId } = ctx;
86
+
87
+ // 查询用户的订阅
88
+ const [userSubscription] = await db
89
+ .select({ subscriptionId: subscription.subscriptionId })
90
+ .from(subscription)
91
+ .where(eq(subscription.userId, userId))
92
+ .limit(1);
93
+
94
+ if (!userSubscription?.subscriptionId) {
95
+ throw new Error("您还没有订阅任何计划");
96
+ }
97
+
98
+ await paymentService.cancelSubscription(userSubscription.subscriptionId);
99
+
100
+ // 更新数据库状态
101
+ await db
102
+ .update(subscription)
103
+ .set({
104
+ cancelAtPeriodEnd: true,
105
+ updatedAt: new Date(),
106
+ })
107
+ .where(eq(subscription.userId, userId));
108
+
109
+ logEvent("payment.subscription.canceled", {
110
+ userId,
111
+ subscriptionId: userSubscription.subscriptionId,
112
+ });
113
+
114
+ return { success: true };
115
+ });
116
+
117
+ /**
118
+ * 获取用户当前订阅状态
119
+ *
120
+ * 用于在前端显示用户的订阅信息
121
+ */
122
+ export const getUserSubscription = protectedAction
123
+ .metadata({ action: "payment.getUserSubscription" })
124
+ .action(async ({ ctx }) => {
125
+ const { userId } = ctx;
126
+
127
+ // 查询用户的订阅信息
128
+ const [userSubscription] = await db
129
+ .select()
130
+ .from(subscription)
131
+ .where(eq(subscription.userId, userId))
132
+ .limit(1);
133
+
134
+ if (!userSubscription) {
135
+ return { subscription: null };
136
+ }
137
+
138
+ // 检查订阅是否有效
139
+ const isActive = ["active", "trialing"].includes(userSubscription.status);
140
+ const isTrialing = userSubscription.status === "trialing";
141
+
142
+ return {
143
+ subscription: {
144
+ id: userSubscription.id,
145
+ status: userSubscription.status,
146
+ priceId: userSubscription.priceId,
147
+ currentPeriodStart: userSubscription.currentPeriodStart,
148
+ currentPeriodEnd: userSubscription.currentPeriodEnd,
149
+ cancelAtPeriodEnd: userSubscription.cancelAtPeriodEnd,
150
+ isActive,
151
+ isTrialing,
152
+ },
153
+ };
154
+ });
155
+
156
+ /**
157
+ * 检查用户是否有有效订阅
158
+ */
159
+ export const hasActiveSubscription = protectedAction
160
+ .metadata({ action: "payment.hasActiveSubscription" })
161
+ .action(async ({ ctx }) => {
162
+ const { userId } = ctx;
163
+
164
+ const [userSubscription] = await db
165
+ .select({ status: subscription.status })
166
+ .from(subscription)
167
+ .where(eq(subscription.userId, userId))
168
+ .limit(1);
169
+
170
+ if (!userSubscription) {
171
+ return { hasSubscription: false, status: null };
172
+ }
173
+
174
+ const isActive = ["active", "trialing", "lifetime"].includes(
175
+ userSubscription.status
176
+ );
177
+
178
+ return {
179
+ hasSubscription: isActive,
180
+ status: userSubscription.status,
181
+ };
182
+ });
@@ -0,0 +1,4 @@
1
+ // Payment System
2
+ export * from "./actions";
3
+ export * from "./types";
4
+ export { paymentModule } from "./manifest";
@@ -0,0 +1,18 @@
1
+ import { defineModule } from "@/core/modules";
2
+
3
+ export const paymentModule = defineModule({
4
+ id: "payment",
5
+ name: "Payment",
6
+ description: "支付结账、订阅生命周期和 Webhook",
7
+ kind: "optional",
8
+ dependencies: ["subscription"],
9
+ routes: [
10
+ {
11
+ path: "/api/webhooks/payment",
12
+ source: "src/app/api/webhooks/payment/route.ts",
13
+ },
14
+ ],
15
+ navigation: [],
16
+ translations: [],
17
+ schema: [],
18
+ });
@@ -0,0 +1,7 @@
1
+ export type {
2
+ PaymentAdapter,
3
+ PaymentCheckout,
4
+ PaymentSubscription,
5
+ PaymentWebhookEvent,
6
+ } from "@/core/services";
7
+ export { paymentService } from "@/services/payment";
@@ -0,0 +1,202 @@
1
+ /**
2
+ * 支付系统类型定义
3
+ *
4
+ * 定义支付相关的所有类型、枚举和接口
5
+ */
6
+
7
+ import type { PaymentProvider } from "@/core/services";
8
+
9
+ // ============================================
10
+ // 支付类型枚举
11
+ // ============================================
12
+
13
+ /**
14
+ * 支付类型
15
+ */
16
+ export enum PaymentType {
17
+ /** 订阅支付(周期性) */
18
+ SUBSCRIPTION = "subscription",
19
+ /** 一次性支付 */
20
+ ONE_TIME = "one-time",
21
+ }
22
+
23
+ /**
24
+ * 计划周期
25
+ */
26
+ export enum PlanInterval {
27
+ /** 月付 */
28
+ MONTH = "monthly",
29
+ /** 年付 */
30
+ YEAR = "yearly",
31
+ }
32
+
33
+ /**
34
+ * 订阅状态
35
+ */
36
+ export enum SubscriptionStatus {
37
+ /** 活跃 */
38
+ ACTIVE = "active",
39
+ /** 试用中 */
40
+ TRIALING = "trialing",
41
+ /** 已取消 */
42
+ CANCELED = "canceled",
43
+ /** 未完成 */
44
+ INCOMPLETE = "incomplete",
45
+ /** 未完成且已过期 */
46
+ INCOMPLETE_EXPIRED = "incomplete_expired",
47
+ /** 逾期未付 */
48
+ PAST_DUE = "past_due",
49
+ /** 已暂停 */
50
+ PAUSED = "paused",
51
+ /** 未支付 */
52
+ UNPAID = "unpaid",
53
+ }
54
+
55
+ // ============================================
56
+ // 价格和计划接口
57
+ // ============================================
58
+
59
+ /**
60
+ * 价格配置
61
+ */
62
+ export interface PriceConfig {
63
+ /** 支付类型 */
64
+ type: PaymentType;
65
+ /** 支付供应商价格 ID */
66
+ priceId: string;
67
+ /** 金额 */
68
+ amount: number;
69
+ /** 周期(仅订阅类型需要) */
70
+ interval?: PlanInterval;
71
+ /** 试用期天数(可选) */
72
+ trialPeriodDays?: number;
73
+ }
74
+
75
+ /**
76
+ * 计划配置
77
+ */
78
+ export interface PlanConfig {
79
+ /** 计划唯一标识 */
80
+ id: string;
81
+ /** 是否为免费计划 */
82
+ isFree?: boolean;
83
+ /** 是否为终身计划 */
84
+ isLifetime?: boolean;
85
+ /** 是否为企业计划 */
86
+ isEnterprise?: boolean;
87
+ /** 是否为热门/推荐计划 */
88
+ popular?: boolean;
89
+ /** 是否高亮显示 */
90
+ highlighted?: boolean;
91
+ /** 价格列表 */
92
+ prices?: PriceConfig[];
93
+ }
94
+
95
+ /**
96
+ * 计划显示信息(用于定价页面)
97
+ */
98
+ export interface PlanDisplayInfo {
99
+ /** 计划名称 */
100
+ name: string;
101
+ /** 计划描述 */
102
+ description: string;
103
+ /** 功能列表 */
104
+ features: string[];
105
+ /** CTA 按钮文本 */
106
+ cta: string;
107
+ /** 是否使用深色主题 */
108
+ dark?: boolean;
109
+ }
110
+
111
+ /**
112
+ * 完整计划信息(配置 + 显示信息)
113
+ */
114
+ export interface Plan extends PlanConfig, PlanDisplayInfo {}
115
+
116
+ // ============================================
117
+ // 支付配置接口
118
+ // ============================================
119
+
120
+ /**
121
+ * 支付系统配置
122
+ */
123
+ export interface PaymentConfig {
124
+ /** 支付提供商 */
125
+ provider: PaymentProvider;
126
+ /** 货币 */
127
+ currency: string;
128
+ /** 年付折扣百分比 */
129
+ yearlyDiscount: number;
130
+ /** 支付完成后重定向 URL */
131
+ redirectAfterCheckout: string;
132
+ /** 取消支付后重定向 URL */
133
+ redirectAfterCancel: string;
134
+ /** 计划配置 */
135
+ plans: {
136
+ free?: PlanConfig;
137
+ starter?: PlanConfig;
138
+ pro?: PlanConfig;
139
+ ultra?: PlanConfig;
140
+ };
141
+ }
142
+
143
+ /**
144
+ * 定价页面配置
145
+ */
146
+ export interface PricingConfig {
147
+ /** 标题 */
148
+ title: string;
149
+ /** 副标题 */
150
+ subtitle: string;
151
+ /** 周期选项文本 */
152
+ frequencies: [string, string];
153
+ /** 年付折扣百分比 */
154
+ yearlyDiscount: number;
155
+ /** 计划列表 */
156
+ plans: Plan[];
157
+ }
158
+
159
+ // ============================================
160
+ // Server Action 参数接口
161
+ // ============================================
162
+
163
+ /**
164
+ * 创建 Checkout Session 参数
165
+ */
166
+ export interface CreateCheckoutParams {
167
+ /** 价格 ID */
168
+ priceId: string;
169
+ /** 支付类型 */
170
+ type?: PaymentType;
171
+ /** 试用期天数 */
172
+ trialPeriodDays?: number;
173
+ /** 成功回调 URL */
174
+ successUrl?: string;
175
+ /** 取消回调 URL */
176
+ cancelUrl?: string;
177
+ }
178
+
179
+ /**
180
+ * 创建 Customer Portal 参数
181
+ */
182
+ export interface CreatePortalParams {
183
+ /** 返回 URL */
184
+ returnUrl?: string;
185
+ }
186
+
187
+ // ============================================
188
+ // Webhook 事件数据接口
189
+ // ============================================
190
+
191
+ /**
192
+ * 订阅数据
193
+ */
194
+ export interface SubscriptionData {
195
+ id: string;
196
+ status: string;
197
+ priceId: string;
198
+ customerId: string;
199
+ currentPeriodStart: Date | null;
200
+ currentPeriodEnd: Date | null;
201
+ cancelAtPeriodEnd: boolean;
202
+ }
@@ -0,0 +1,6 @@
1
+ export { PseoCta } from "./pseo-cta";
2
+ export { PseoFaq } from "./pseo-faq";
3
+ export { PseoFeatureGrid } from "./pseo-feature-grid";
4
+ export { PseoHero } from "./pseo-hero";
5
+ export { PseoRelated } from "./pseo-related";
6
+ export { PseoUseCases } from "./pseo-use-cases";
@@ -0,0 +1,51 @@
1
+ import { MessageCircle } from "lucide-react";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Link } from "@/i18n/routing";
5
+
6
+ import type { PseoPage } from "../lib/pseo-data";
7
+
8
+ export function PseoCta({ page }: { page: PseoPage }) {
9
+ const { cta } = page.data;
10
+
11
+ return (
12
+ <section className="container py-24" id="cta">
13
+ <div className="mx-auto max-w-4xl">
14
+ <div className="relative overflow-hidden rounded-3xl bg-primary px-8 py-12 text-center text-primary-foreground md:px-16 md:py-16">
15
+ <div className="absolute -left-24 -top-24 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
16
+ <div className="absolute -bottom-24 -right-24 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
17
+
18
+ <div className="relative">
19
+ <h2 className="mb-4 text-balance text-3xl font-bold tracking-tight md:text-4xl">
20
+ {cta.title}
21
+ </h2>
22
+ <p className="mx-auto mb-8 max-w-2xl text-primary-foreground/80">
23
+ {cta.description}
24
+ </p>
25
+
26
+ <div className="mb-6 flex flex-col justify-center gap-3 sm:flex-row">
27
+ <Button size="lg" className="gap-2 bg-white text-primary" asChild>
28
+ <Link href={cta.primaryCta.href}>
29
+ <MessageCircle className="h-4 w-4" />
30
+ {cta.primaryCta.label}
31
+ </Link>
32
+ </Button>
33
+ <Button
34
+ size="lg"
35
+ variant="outline"
36
+ className="border-white/40 text-white hover:bg-white/10"
37
+ asChild
38
+ >
39
+ <Link href={cta.secondaryCta.href}>
40
+ {cta.secondaryCta.label}
41
+ </Link>
42
+ </Button>
43
+ </div>
44
+
45
+ <p className="text-sm text-primary-foreground/70">{cta.note}</p>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </section>
50
+ );
51
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ Accordion,
3
+ AccordionContent,
4
+ AccordionItem,
5
+ AccordionTrigger,
6
+ } from "@/components/ui/accordion";
7
+
8
+ import type { PseoPage } from "../lib/pseo-data";
9
+
10
+ export function PseoFaq({ page }: { page: PseoPage }) {
11
+ const { sections, faq } = page.data;
12
+
13
+ return (
14
+ <section className="container py-24" id="faq">
15
+ <div className="mx-auto max-w-3xl">
16
+ <div className="mb-12 text-center">
17
+ <h2 className="mb-4 text-balance text-3xl font-bold tracking-tight md:text-4xl">
18
+ {sections.faq.title}
19
+ </h2>
20
+ <p className="mx-auto max-w-2xl text-muted-foreground">
21
+ {sections.faq.subtitle}
22
+ </p>
23
+ </div>
24
+
25
+ <Accordion type="single" collapsible className="w-full">
26
+ {faq.map((item, index) => (
27
+ <AccordionItem key={item.question} value={`item-${index}`}>
28
+ <AccordionTrigger className="text-left">
29
+ {item.question}
30
+ </AccordionTrigger>
31
+ <AccordionContent className="text-muted-foreground">
32
+ {item.answer}
33
+ </AccordionContent>
34
+ </AccordionItem>
35
+ ))}
36
+ </Accordion>
37
+ </div>
38
+ </section>
39
+ );
40
+ }