kofi-stack-template-generator 2.1.37 → 2.1.39

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 (323) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/dist/index.js +8057 -440
  3. package/package.json +1 -1
  4. package/src/templates.generated.ts +248 -94
  5. package/templates/integrations/posthog/src/components/providers/posthog-provider.tsx.hbs +4 -1
  6. package/templates/marketing/payload/package.json.hbs +41 -26
  7. package/templates/marketing/payload/src/Footer/Component.client.tsx +288 -0
  8. package/templates/marketing/payload/src/Footer/Component.tsx +11 -0
  9. package/templates/marketing/payload/src/Footer/RowLabel.tsx +15 -0
  10. package/templates/marketing/payload/src/Footer/config.ts +178 -0
  11. package/templates/marketing/payload/src/Footer/hooks/{revalidateFooter.ts.hbs → revalidateFooter.ts} +5 -5
  12. package/templates/marketing/payload/src/Header/Component.client.tsx +94 -0
  13. package/templates/marketing/payload/src/Header/Component.tsx +10 -0
  14. package/templates/marketing/payload/src/Header/MegaMenu/index.tsx +197 -0
  15. package/templates/marketing/payload/src/Header/MobileMenu/HamburgerIcon.tsx +48 -0
  16. package/templates/marketing/payload/src/Header/MobileMenu/index.tsx +299 -0
  17. package/templates/marketing/payload/src/Header/Nav/index.tsx +76 -0
  18. package/templates/marketing/payload/src/Header/RowLabel.tsx +21 -0
  19. package/templates/marketing/payload/src/Header/config.ts +208 -0
  20. package/templates/marketing/payload/src/Header/hooks/{revalidateHeader.ts.hbs → revalidateHeader.ts} +5 -5
  21. package/templates/marketing/payload/src/access/{authenticated.ts.hbs → authenticated.ts} +1 -1
  22. package/templates/marketing/payload/src/access/{authenticatedOrPublished.ts.hbs → authenticatedOrPublished.ts} +8 -8
  23. package/templates/marketing/payload/src/app/(docs)/docs/[[...slug]]/page.tsx +117 -0
  24. package/templates/marketing/payload/src/app/(docs)/docs/layout.tsx +39 -0
  25. package/templates/marketing/payload/src/app/(docs)/layout.tsx +44 -0
  26. package/templates/marketing/payload/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts +68 -0
  27. package/templates/marketing/payload/src/app/(frontend)/(sitemaps)/posts-sitemap.xml/route.ts +55 -0
  28. package/templates/marketing/payload/src/app/(frontend)/[slug]/page.client.tsx +15 -0
  29. package/templates/marketing/payload/src/app/(frontend)/[slug]/page.tsx +114 -0
  30. package/templates/marketing/payload/src/app/(frontend)/api/docs-search/route.ts +67 -0
  31. package/templates/marketing/payload/src/app/(frontend)/api/newsletter/route.ts +260 -0
  32. package/templates/marketing/payload/src/app/(frontend)/api/pricing/route.ts +266 -0
  33. package/templates/marketing/payload/src/app/(frontend)/globals.css +1019 -0
  34. package/templates/marketing/payload/src/app/(frontend)/layout.tsx +114 -0
  35. package/templates/marketing/payload/src/app/(frontend)/next/exit-preview/route.ts +7 -0
  36. package/templates/marketing/payload/src/app/(frontend)/next/preview/route.ts +56 -0
  37. package/templates/marketing/payload/src/app/(frontend)/next/seed/route.ts +31 -0
  38. package/templates/marketing/payload/src/app/(frontend)/not-found.tsx +17 -0
  39. package/templates/marketing/payload/src/app/(frontend)/page.tsx +5 -0
  40. package/templates/marketing/payload/src/app/(frontend)/posts/BlogPageClient.tsx +190 -0
  41. package/templates/marketing/payload/src/app/(frontend)/posts/[slug]/BlogPostContent.tsx +67 -0
  42. package/templates/marketing/payload/src/app/(frontend)/posts/[slug]/page.client.tsx +15 -0
  43. package/templates/marketing/payload/src/app/(frontend)/posts/[slug]/page.tsx +118 -0
  44. package/templates/marketing/payload/src/app/(frontend)/posts/page/[pageNumber]/page.client.tsx +15 -0
  45. package/templates/marketing/payload/src/app/(frontend)/posts/page/[pageNumber]/page.tsx +87 -0
  46. package/templates/marketing/payload/src/app/(frontend)/posts/page.tsx +49 -0
  47. package/templates/marketing/payload/src/app/(frontend)/search/page.client.tsx +15 -0
  48. package/templates/marketing/payload/src/app/(frontend)/search/page.tsx +87 -0
  49. package/templates/marketing/payload/src/app/(payload)/admin/[[...segments]]/not-found.tsx +24 -0
  50. package/templates/marketing/payload/src/app/(payload)/admin/[[...segments]]/page.tsx +24 -0
  51. package/templates/marketing/payload/src/app/(payload)/admin/importMap.js +83 -0
  52. package/templates/marketing/payload/src/app/(payload)/api/[...slug]/{route.ts.hbs → route.ts} +13 -9
  53. package/templates/marketing/payload/src/app/(payload)/api/graphql/{route.ts.hbs → route.ts} +2 -2
  54. package/templates/marketing/payload/src/app/(payload)/api/graphql-playground/{route.ts.hbs → route.ts} +3 -3
  55. package/templates/marketing/payload/src/app/(payload)/custom.scss +0 -0
  56. package/templates/marketing/payload/src/app/(payload)/layout.tsx +31 -0
  57. package/templates/marketing/payload/src/blocks/ArchiveBlock/Component.tsx +65 -0
  58. package/templates/marketing/payload/src/blocks/ArchiveBlock/config.ts +120 -0
  59. package/templates/marketing/payload/src/blocks/Banner/Component.tsx +26 -0
  60. package/templates/marketing/payload/src/blocks/Banner/config.ts +67 -0
  61. package/templates/marketing/payload/src/blocks/BentoFeatures/Component.tsx +243 -0
  62. package/templates/marketing/payload/src/blocks/BentoFeatures/config.ts +147 -0
  63. package/templates/marketing/payload/src/blocks/CallToAction/Component.tsx +31 -0
  64. package/templates/marketing/payload/src/blocks/CallToAction/config.ts +68 -0
  65. package/templates/marketing/payload/src/blocks/Code/Component.client.tsx +33 -0
  66. package/templates/marketing/payload/src/blocks/Code/Component.tsx +21 -0
  67. package/templates/marketing/payload/src/blocks/Code/CopyButton.tsx +33 -0
  68. package/templates/marketing/payload/src/blocks/Code/config.ts +33 -0
  69. package/templates/marketing/payload/src/blocks/Content/Component.tsx +41 -0
  70. package/templates/marketing/payload/src/blocks/Content/config.ts +105 -0
  71. package/templates/marketing/payload/src/blocks/FAQAccordion/Component.tsx +90 -0
  72. package/templates/marketing/payload/src/blocks/FAQAccordion/config.ts +75 -0
  73. package/templates/marketing/payload/src/blocks/FeatureGrid/Component.tsx +124 -0
  74. package/templates/marketing/payload/src/blocks/FeatureGrid/config.ts +120 -0
  75. package/templates/marketing/payload/src/blocks/FeatureShowcase/Component.tsx +107 -0
  76. package/templates/marketing/payload/src/blocks/FeatureShowcase/config.ts +111 -0
  77. package/templates/marketing/payload/src/blocks/FinalCTA/Component.tsx +117 -0
  78. package/templates/marketing/payload/src/blocks/FinalCTA/config.ts +50 -0
  79. package/templates/marketing/payload/src/blocks/Form/Checkbox/index.tsx +45 -0
  80. package/templates/marketing/payload/src/blocks/Form/Component.tsx +170 -0
  81. package/templates/marketing/payload/src/blocks/Form/Country/index.tsx +65 -0
  82. package/templates/marketing/payload/src/blocks/Form/Country/options.ts +982 -0
  83. package/templates/marketing/payload/src/blocks/Form/Email/index.tsx +38 -0
  84. package/templates/marketing/payload/src/blocks/Form/Error/index.tsx +13 -0
  85. package/templates/marketing/payload/src/blocks/Form/Message/index.tsx +13 -0
  86. package/templates/marketing/payload/src/blocks/Form/Number/index.tsx +36 -0
  87. package/templates/marketing/payload/src/blocks/Form/Select/index.tsx +63 -0
  88. package/templates/marketing/payload/src/blocks/Form/State/index.tsx +64 -0
  89. package/templates/marketing/payload/src/blocks/Form/State/options.ts +52 -0
  90. package/templates/marketing/payload/src/blocks/Form/Text/index.tsx +32 -0
  91. package/templates/marketing/payload/src/blocks/Form/Textarea/index.tsx +40 -0
  92. package/templates/marketing/payload/src/blocks/Form/Width/index.tsx +13 -0
  93. package/templates/marketing/payload/src/blocks/Form/config.ts +77 -0
  94. package/templates/marketing/payload/src/blocks/Form/fields.tsx +21 -0
  95. package/templates/marketing/payload/src/blocks/HowItWorks/Component.tsx +59 -0
  96. package/templates/marketing/payload/src/blocks/HowItWorks/config.ts +88 -0
  97. package/templates/marketing/payload/src/blocks/IndustryTabs/Component.tsx +132 -0
  98. package/templates/marketing/payload/src/blocks/IndustryTabs/config.ts +77 -0
  99. package/templates/marketing/payload/src/blocks/LogoBanner/Component.tsx +95 -0
  100. package/templates/marketing/payload/src/blocks/LogoBanner/config.ts +48 -0
  101. package/templates/marketing/payload/src/blocks/MediaBlock/Component.tsx +67 -0
  102. package/templates/marketing/payload/src/blocks/MediaBlock/config.ts +14 -0
  103. package/templates/marketing/payload/src/blocks/Personas/Component.tsx +69 -0
  104. package/templates/marketing/payload/src/blocks/Personas/config.ts +96 -0
  105. package/templates/marketing/payload/src/blocks/PricingTable/ComparisonTable.tsx +250 -0
  106. package/templates/marketing/payload/src/blocks/PricingTable/Component.tsx +443 -0
  107. package/templates/marketing/payload/src/blocks/PricingTable/config.ts +142 -0
  108. package/templates/marketing/payload/src/blocks/ProofBanner/Component.tsx +65 -0
  109. package/templates/marketing/payload/src/blocks/ProofBanner/config.ts +42 -0
  110. package/templates/marketing/payload/src/blocks/RelatedPosts/Component.tsx +32 -0
  111. package/templates/marketing/payload/src/blocks/RenderBlocks.tsx +92 -0
  112. package/templates/marketing/payload/src/blocks/TestimonialsGrid/Component.tsx +107 -0
  113. package/templates/marketing/payload/src/blocks/TestimonialsGrid/config.ts +76 -0
  114. package/templates/marketing/payload/src/blocks/TrustColumns/Component.tsx +83 -0
  115. package/templates/marketing/payload/src/blocks/TrustColumns/config.ts +70 -0
  116. package/templates/marketing/payload/src/collections/Categories.ts +28 -0
  117. package/templates/marketing/payload/src/collections/FAQs/index.ts +100 -0
  118. package/templates/marketing/payload/src/collections/Media.ts +160 -0
  119. package/templates/marketing/payload/src/collections/Pages/hooks/revalidatePage.ts +43 -0
  120. package/templates/marketing/payload/src/collections/Pages/index.ts +168 -0
  121. package/templates/marketing/payload/src/collections/Posts/hooks/populateAuthors.ts +41 -0
  122. package/templates/marketing/payload/src/collections/Posts/hooks/revalidatePost.ts +44 -0
  123. package/templates/marketing/payload/src/collections/Posts/index.ts +259 -0
  124. package/templates/marketing/payload/src/collections/Users/index.ts +26 -0
  125. package/templates/marketing/payload/src/components/AdminBar/index.scss +7 -0
  126. package/templates/marketing/payload/src/components/AdminBar/index.tsx +89 -0
  127. package/templates/marketing/payload/src/components/Analytics/CTATracker.tsx +33 -0
  128. package/templates/marketing/payload/src/components/Analytics/FeatureSectionTracker.tsx +47 -0
  129. package/templates/marketing/payload/src/components/Analytics/PricingViewTracker.tsx +46 -0
  130. package/templates/marketing/payload/src/components/Analytics/index.tsx +3 -0
  131. package/templates/marketing/payload/src/components/BeforeDashboard/SeedButton/index.tsx +89 -0
  132. package/templates/marketing/payload/src/components/BeforeDashboard/index.tsx +69 -0
  133. package/templates/marketing/payload/src/components/BeforeLogin/index.tsx +14 -0
  134. package/templates/marketing/payload/src/components/BlogCTA/index.tsx +77 -0
  135. package/templates/marketing/payload/src/components/Card/index.tsx +85 -0
  136. package/templates/marketing/payload/src/components/CollectionArchive/index.tsx +32 -0
  137. package/templates/marketing/payload/src/components/JsonLd/index.tsx +138 -0
  138. package/templates/marketing/payload/src/components/Link/index.tsx +66 -0
  139. package/templates/marketing/payload/src/components/LivePreviewListener/index.tsx +10 -0
  140. package/templates/marketing/payload/src/components/Logo/Logo.tsx +46 -0
  141. package/templates/marketing/payload/src/components/Media/ImageMedia/index.tsx +80 -0
  142. package/templates/marketing/payload/src/components/Media/VideoMedia/index.tsx +47 -0
  143. package/templates/marketing/payload/src/components/Media/index.tsx +26 -0
  144. package/templates/marketing/payload/src/components/Media/types.ts +22 -0
  145. package/templates/marketing/payload/src/components/PageRange/index.tsx +57 -0
  146. package/templates/marketing/payload/src/components/Pagination/index.tsx +101 -0
  147. package/templates/marketing/payload/src/components/PayloadRedirects/index.tsx +48 -0
  148. package/templates/marketing/payload/src/components/RichText/index.tsx +152 -0
  149. package/templates/marketing/payload/src/components/TableOfContents/index.tsx +128 -0
  150. package/templates/marketing/payload/src/components/ui/accordion.tsx +64 -0
  151. package/templates/marketing/payload/src/components/ui/button.tsx +52 -0
  152. package/templates/marketing/payload/src/components/ui/card.tsx +48 -0
  153. package/templates/marketing/payload/src/components/ui/checkbox.tsx +27 -0
  154. package/templates/marketing/payload/src/components/ui/input.tsx +22 -0
  155. package/templates/marketing/payload/src/components/ui/label.tsx +19 -0
  156. package/templates/marketing/payload/src/components/ui/pagination.tsx +92 -0
  157. package/templates/marketing/payload/src/components/ui/select.tsx +144 -0
  158. package/templates/marketing/payload/src/components/ui/textarea.tsx +21 -0
  159. package/templates/marketing/payload/src/endpoints/seed/contact-form.ts +111 -0
  160. package/templates/marketing/payload/src/endpoints/seed/contact-page.ts +56 -0
  161. package/templates/marketing/payload/src/endpoints/seed/directoryhub/about.ts +281 -0
  162. package/templates/marketing/payload/src/endpoints/seed/directoryhub/faqs.ts +224 -0
  163. package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/automation.ts +229 -0
  164. package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/custom-fields.ts +229 -0
  165. package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/dashboard.ts +228 -0
  166. package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/index.ts +6 -0
  167. package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/monetization.ts +230 -0
  168. package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/seo.ts +229 -0
  169. package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/templates.ts +218 -0
  170. package/templates/marketing/payload/src/endpoints/seed/directoryhub/home.ts +555 -0
  171. package/templates/marketing/payload/src/endpoints/seed/directoryhub/index.ts +767 -0
  172. package/templates/marketing/payload/src/endpoints/seed/directoryhub/posts.ts +623 -0
  173. package/templates/marketing/payload/src/endpoints/seed/directoryhub/pricing.ts +251 -0
  174. package/templates/marketing/payload/src/endpoints/seed/directoryhub/privacy.ts +457 -0
  175. package/templates/marketing/payload/src/endpoints/seed/directoryhub/richtext-helper.ts +88 -0
  176. package/templates/marketing/payload/src/endpoints/seed/directoryhub/terms.ts +478 -0
  177. package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/b2b-vendor-hubs.ts +229 -0
  178. package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/communities.ts +230 -0
  179. package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/index.ts +4 -0
  180. package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/local-services.ts +230 -0
  181. package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/marketplaces.ts +230 -0
  182. package/templates/marketing/payload/src/endpoints/seed/home-static.ts +691 -0
  183. package/templates/marketing/payload/src/endpoints/seed/home.ts +675 -0
  184. package/templates/marketing/payload/src/endpoints/seed/image-1.ts +67 -0
  185. package/templates/marketing/payload/src/endpoints/seed/image-2.ts +67 -0
  186. package/templates/marketing/payload/src/endpoints/seed/image-3.ts +67 -0
  187. package/templates/marketing/payload/src/endpoints/seed/image-hero-1.ts +5 -0
  188. package/templates/marketing/payload/src/endpoints/seed/image-hero1.webp +0 -0
  189. package/templates/marketing/payload/src/endpoints/seed/image-post1.webp +0 -0
  190. package/templates/marketing/payload/src/endpoints/seed/image-post2.webp +0 -0
  191. package/templates/marketing/payload/src/endpoints/seed/image-post3.webp +0 -0
  192. package/templates/marketing/payload/src/endpoints/seed/index.ts +335 -0
  193. package/templates/marketing/payload/src/endpoints/seed/post-1.ts +315 -0
  194. package/templates/marketing/payload/src/endpoints/seed/post-2.ts +232 -0
  195. package/templates/marketing/payload/src/endpoints/seed/post-3.ts +268 -0
  196. package/templates/marketing/payload/src/fields/defaultLexical.ts +73 -0
  197. package/templates/marketing/payload/src/fields/link.ts +139 -0
  198. package/templates/marketing/payload/src/fields/linkGroup.ts +28 -0
  199. package/templates/marketing/payload/src/heros/HighImpact/index.tsx +56 -0
  200. package/templates/marketing/payload/src/heros/LowImpact/index.tsx +48 -0
  201. package/templates/marketing/payload/src/heros/MediumImpact/index.tsx +50 -0
  202. package/templates/marketing/payload/src/heros/PostHero/index.tsx +73 -0
  203. package/templates/marketing/payload/src/heros/ProductShowcase/AnimatedMockup.tsx +241 -0
  204. package/templates/marketing/payload/src/heros/ProductShowcase/index.tsx +108 -0
  205. package/templates/marketing/payload/src/heros/{RenderHero.tsx.hbs → RenderHero.tsx} +9 -9
  206. package/templates/marketing/payload/src/heros/config.ts +121 -0
  207. package/templates/marketing/payload/src/hooks/populatePublishedAt.ts +15 -0
  208. package/templates/marketing/payload/src/hooks/{revalidateRedirects.ts.hbs → revalidateRedirects.ts} +3 -3
  209. package/templates/marketing/payload/src/lib/convex.ts +13 -0
  210. package/templates/marketing/payload/src/lib/docs-source.ts +138 -0
  211. package/templates/marketing/payload/src/lib/mdx.tsx +191 -0
  212. package/templates/marketing/payload/src/payload.config.ts.hbs +95 -145
  213. package/templates/marketing/payload/src/plugins/index.ts +107 -0
  214. package/templates/marketing/payload/src/providers/HeaderTheme/index.tsx +34 -0
  215. package/templates/marketing/payload/src/providers/PostHogProvider.tsx +33 -0
  216. package/templates/marketing/payload/src/providers/Theme/InitTheme/{index.tsx.hbs → index.tsx} +11 -10
  217. package/templates/marketing/payload/src/providers/Theme/ThemeSelector/index.tsx +133 -0
  218. package/templates/marketing/payload/src/providers/Theme/ThemeSelector/types.ts +7 -0
  219. package/templates/marketing/payload/src/providers/Theme/index.tsx +60 -0
  220. package/templates/marketing/payload/src/providers/Theme/shared.ts +17 -0
  221. package/templates/marketing/payload/src/providers/Theme/{types.ts.hbs → types.ts} +3 -3
  222. package/templates/marketing/payload/src/providers/index.tsx +17 -0
  223. package/templates/marketing/payload/src/search/Component.tsx +42 -0
  224. package/templates/marketing/payload/src/search/beforeSync.ts +56 -0
  225. package/templates/marketing/payload/src/search/fieldOverrides.ts +61 -0
  226. package/templates/marketing/payload/src/utilities/deepMerge.ts +35 -0
  227. package/templates/marketing/payload/src/utilities/extractHeadings.ts +78 -0
  228. package/templates/marketing/payload/src/utilities/formatAuthors.ts +24 -0
  229. package/templates/marketing/payload/src/utilities/formatDateTime.ts +20 -0
  230. package/templates/marketing/payload/src/utilities/generateMeta.ts +93 -0
  231. package/templates/marketing/payload/src/utilities/generatePreviewPath.ts +33 -0
  232. package/templates/marketing/payload/src/utilities/getDocument.ts +32 -0
  233. package/templates/marketing/payload/src/utilities/getGlobals.ts +26 -0
  234. package/templates/marketing/payload/src/utilities/getMeUser.ts +43 -0
  235. package/templates/marketing/payload/src/utilities/getMediaUrl.ts +24 -0
  236. package/templates/marketing/payload/src/utilities/getRedirects.ts +26 -0
  237. package/templates/marketing/payload/src/utilities/getURL.ts +26 -0
  238. package/templates/marketing/payload/src/utilities/mergeOpenGraph.ts +26 -0
  239. package/templates/marketing/payload/src/utilities/toKebabCase.ts +5 -0
  240. package/templates/marketing/payload/src/utilities/ui.ts +12 -0
  241. package/templates/marketing/payload/src/utilities/useClickableCard.ts +108 -0
  242. package/templates/marketing/payload/src/utilities/useDebounce.ts +17 -0
  243. package/templates/packages/ui/src/components/button.tsx.hbs +53 -0
  244. package/templates/packages/ui/src/components/card.tsx.hbs +76 -0
  245. package/templates/packages/ui/src/components/separator.tsx.hbs +26 -0
  246. package/templates/marketing/payload/src/Footer/config.ts.hbs +0 -178
  247. package/templates/marketing/payload/src/Footer/index.ts.hbs +0 -1
  248. package/templates/marketing/payload/src/Header/RowLabel.tsx.hbs +0 -21
  249. package/templates/marketing/payload/src/Header/config.ts.hbs +0 -208
  250. package/templates/marketing/payload/src/Header/index.ts.hbs +0 -1
  251. package/templates/marketing/payload/src/access/index.ts.hbs +0 -3
  252. package/templates/marketing/payload/src/app/(frontend)/layout.tsx.hbs +0 -19
  253. package/templates/marketing/payload/src/app/(frontend)/next/seed/route.ts.hbs +0 -31
  254. package/templates/marketing/payload/src/app/(frontend)/page.tsx.hbs +0 -83
  255. package/templates/marketing/payload/src/app/(payload)/admin/[[...segments]]/not-found.tsx.hbs +0 -24
  256. package/templates/marketing/payload/src/app/(payload)/admin/[[...segments]]/page.tsx.hbs +0 -24
  257. package/templates/marketing/payload/src/app/(payload)/admin/importMap.js.hbs +0 -1
  258. package/templates/marketing/payload/src/app/(payload)/custom.scss.hbs +0 -1
  259. package/templates/marketing/payload/src/app/(payload)/layout.tsx.hbs +0 -31
  260. package/templates/marketing/payload/src/app/globals.css.hbs +0 -83
  261. package/templates/marketing/payload/src/app/layout.tsx.hbs +0 -10
  262. package/templates/marketing/payload/src/blocks/Benefits.ts.hbs +0 -34
  263. package/templates/marketing/payload/src/blocks/CTA.ts.hbs +0 -39
  264. package/templates/marketing/payload/src/blocks/Content.ts.hbs +0 -9
  265. package/templates/marketing/payload/src/blocks/FAQ.ts.hbs +0 -18
  266. package/templates/marketing/payload/src/blocks/Features.ts.hbs +0 -34
  267. package/templates/marketing/payload/src/blocks/Hero.ts.hbs +0 -40
  268. package/templates/marketing/payload/src/blocks/LogoBanner.ts.hbs +0 -17
  269. package/templates/marketing/payload/src/blocks/Pricing.ts.hbs +0 -37
  270. package/templates/marketing/payload/src/blocks/Testimonials.ts.hbs +0 -21
  271. package/templates/marketing/payload/src/blocks/index.ts.hbs +0 -9
  272. package/templates/marketing/payload/src/collections/Categories/index.ts.hbs +0 -28
  273. package/templates/marketing/payload/src/collections/FAQs/index.ts.hbs +0 -100
  274. package/templates/marketing/payload/src/collections/Media.ts.hbs +0 -164
  275. package/templates/marketing/payload/src/collections/Pages/hooks/revalidatePage.ts.hbs +0 -43
  276. package/templates/marketing/payload/src/collections/Pages/index.ts.hbs +0 -142
  277. package/templates/marketing/payload/src/collections/Posts/hooks/populateAuthors.ts.hbs +0 -41
  278. package/templates/marketing/payload/src/collections/Posts/hooks/revalidatePost.ts.hbs +0 -44
  279. package/templates/marketing/payload/src/collections/Posts/index.ts.hbs +0 -244
  280. package/templates/marketing/payload/src/collections/Users/index.ts.hbs +0 -26
  281. package/templates/marketing/payload/src/collections/index.ts.hbs +0 -6
  282. package/templates/marketing/payload/src/components/BeforeDashboard/SeedButton/index.tsx.hbs +0 -89
  283. package/templates/marketing/payload/src/components/BeforeDashboard/index.tsx.hbs +0 -69
  284. package/templates/marketing/payload/src/components/BeforeLogin/index.tsx.hbs +0 -14
  285. package/templates/marketing/payload/src/components/Link/index.tsx.hbs +0 -79
  286. package/templates/marketing/payload/src/components/Media/index.tsx.hbs +0 -67
  287. package/templates/marketing/payload/src/components/RichText/index.tsx.hbs +0 -44
  288. package/templates/marketing/payload/src/endpoints/seed/home.ts.hbs +0 -76
  289. package/templates/marketing/payload/src/endpoints/seed/image-1.ts.hbs +0 -5
  290. package/templates/marketing/payload/src/endpoints/seed/image-2.ts.hbs +0 -5
  291. package/templates/marketing/payload/src/endpoints/seed/image-hero.ts.hbs +0 -5
  292. package/templates/marketing/payload/src/endpoints/seed/index.ts.hbs +0 -235
  293. package/templates/marketing/payload/src/endpoints/seed/post-1.ts.hbs +0 -252
  294. package/templates/marketing/payload/src/fields/defaultLexical.ts.hbs +0 -73
  295. package/templates/marketing/payload/src/fields/link.ts.hbs +0 -139
  296. package/templates/marketing/payload/src/fields/linkGroup.ts.hbs +0 -28
  297. package/templates/marketing/payload/src/globals/index.ts.hbs +0 -2
  298. package/templates/marketing/payload/src/heros/HighImpact/index.tsx.hbs +0 -53
  299. package/templates/marketing/payload/src/heros/LowImpact/index.tsx.hbs +0 -48
  300. package/templates/marketing/payload/src/heros/MediumImpact/index.tsx.hbs +0 -46
  301. package/templates/marketing/payload/src/heros/PostHero/index.tsx.hbs +0 -68
  302. package/templates/marketing/payload/src/heros/ProductShowcase/index.tsx.hbs +0 -88
  303. package/templates/marketing/payload/src/heros/config.ts.hbs +0 -112
  304. package/templates/marketing/payload/src/heros/index.ts.hbs +0 -7
  305. package/templates/marketing/payload/src/hooks/index.ts.hbs +0 -2
  306. package/templates/marketing/payload/src/hooks/populatePublishedAt.ts.hbs +0 -15
  307. package/templates/marketing/payload/src/providers/HeaderTheme/index.tsx.hbs +0 -34
  308. package/templates/marketing/payload/src/providers/Theme/index.tsx.hbs +0 -60
  309. package/templates/marketing/payload/src/providers/Theme/shared.ts.hbs +0 -17
  310. package/templates/marketing/payload/src/providers/index.tsx.hbs +0 -18
  311. package/templates/marketing/payload/src/utilities/deepMerge.ts.hbs +0 -35
  312. package/templates/marketing/payload/src/utilities/formatAuthors.ts.hbs +0 -24
  313. package/templates/marketing/payload/src/utilities/formatDateTime.ts.hbs +0 -13
  314. package/templates/marketing/payload/src/utilities/generateMeta.ts.hbs +0 -87
  315. package/templates/marketing/payload/src/utilities/generatePreviewPath.ts.hbs +0 -33
  316. package/templates/marketing/payload/src/utilities/getURL.ts.hbs +0 -26
  317. package/templates/marketing/payload/src/utilities/index.ts.hbs +0 -8
  318. package/templates/marketing/payload/src/utilities/mergeOpenGraph.ts.hbs +0 -26
  319. /package/templates/marketing/payload/src/access/{anyone.ts.hbs → anyone.ts} +0 -0
  320. /package/templates/marketing/payload/src/components/BeforeDashboard/SeedButton/{index.scss.hbs → index.scss} +0 -0
  321. /package/templates/marketing/payload/src/components/BeforeDashboard/{index.scss.hbs → index.scss} +0 -0
  322. /package/templates/marketing/payload/src/fields/{index.ts.hbs → index.ts} +0 -0
  323. /package/templates/marketing/payload/src/utilities/{canUseDOM.ts.hbs → canUseDOM.ts} +0 -0
@@ -0,0 +1,114 @@
1
+ import type { Metadata } from "next"
2
+
3
+ import { PayloadRedirects } from "@/components/PayloadRedirects"
4
+ import { homeStatic } from "@/endpoints/seed/home-static"
5
+ import configPromise from "@payload-config"
6
+ import { draftMode } from "next/headers"
7
+ import { type RequiredDataFromCollectionSlug, getPayload } from "payload"
8
+ import { cache } from "react"
9
+
10
+ import { RenderBlocks } from "@/blocks/RenderBlocks"
11
+ import { LivePreviewListener } from "@/components/LivePreviewListener"
12
+ import { RenderHero } from "@/heros/RenderHero"
13
+ import { generateMeta } from "@/utilities/generateMeta"
14
+ import PageClient from "./page.client"
15
+
16
+ export async function generateStaticParams() {
17
+ const payload = await getPayload({ config: configPromise })
18
+ const pages = await payload.find({
19
+ collection: "pages",
20
+ draft: false,
21
+ limit: 1000,
22
+ overrideAccess: false,
23
+ pagination: false,
24
+ select: {
25
+ slug: true,
26
+ },
27
+ })
28
+
29
+ const params = pages.docs
30
+ ?.filter((doc) => {
31
+ return doc.slug !== "home"
32
+ })
33
+ .map(({ slug }) => {
34
+ return { slug }
35
+ })
36
+
37
+ return params
38
+ }
39
+
40
+ type Args = {
41
+ params: Promise<{
42
+ slug?: string
43
+ }>
44
+ }
45
+
46
+ export default async function Page({ params: paramsPromise }: Args) {
47
+ const { isEnabled: draft } = await draftMode()
48
+ const { slug = "home" } = await paramsPromise
49
+ // Decode to support slugs with special characters
50
+ const decodedSlug = decodeURIComponent(slug)
51
+ const url = `/${decodedSlug}`
52
+ let page: RequiredDataFromCollectionSlug<"pages"> | null
53
+
54
+ page = await queryPageBySlug({
55
+ slug: decodedSlug,
56
+ })
57
+
58
+ // Remove this code once your website is seeded
59
+ if (!page && slug === "home") {
60
+ page = homeStatic
61
+ }
62
+
63
+ if (!page) {
64
+ return <PayloadRedirects url={url} />
65
+ }
66
+
67
+ const { hero, layout } = page
68
+
69
+ return (
70
+ <article className="pt-16 pb-24">
71
+ <PageClient />
72
+ {/* Allows redirects for valid pages too */}
73
+ <PayloadRedirects disableNotFound url={url} />
74
+
75
+ {draft && <LivePreviewListener />}
76
+
77
+ <RenderHero {...hero} />
78
+ <RenderBlocks blocks={layout} />
79
+ </article>
80
+ )
81
+ }
82
+
83
+ export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
84
+ const { slug = "home" } = await paramsPromise
85
+ // Decode to support slugs with special characters
86
+ const decodedSlug = decodeURIComponent(slug)
87
+ const page = await queryPageBySlug({
88
+ slug: decodedSlug,
89
+ })
90
+
91
+ return generateMeta({ doc: page })
92
+ }
93
+
94
+ const queryPageBySlug = cache(async ({ slug }: { slug: string }) => {
95
+ const { isEnabled: draft } = await draftMode()
96
+
97
+ const payload = await getPayload({ config: configPromise })
98
+
99
+ const result = await payload.find({
100
+ collection: "pages",
101
+ depth: 2, // Populate relationships in rich text content
102
+ draft,
103
+ limit: 1,
104
+ pagination: false,
105
+ overrideAccess: draft,
106
+ where: {
107
+ slug: {
108
+ equals: slug,
109
+ },
110
+ },
111
+ })
112
+
113
+ return result.docs?.[0] || null
114
+ })
@@ -0,0 +1,67 @@
1
+ import { getDocsFromConvex } from "@/lib/docs-source"
2
+ import { NextResponse } from "next/server"
3
+
4
+ /**
5
+ * Search API for documentation
6
+ * Returns matching docs based on query
7
+ */
8
+ export async function GET(request: Request) {
9
+ const { searchParams } = new URL(request.url)
10
+ const query = searchParams.get("q")?.toLowerCase() || ""
11
+
12
+ if (!query || query.length < 2) {
13
+ return NextResponse.json({ results: [] })
14
+ }
15
+
16
+ try {
17
+ const docs = await getDocsFromConvex()
18
+
19
+ // Simple search implementation
20
+ const results = docs
21
+ .filter((doc) => {
22
+ const titleMatch = doc.title.toLowerCase().includes(query)
23
+ const descriptionMatch = doc.description?.toLowerCase().includes(query)
24
+ const contentMatch = doc.content.toLowerCase().includes(query)
25
+ return titleMatch || descriptionMatch || contentMatch
26
+ })
27
+ .map((doc) => ({
28
+ id: doc._id,
29
+ title: doc.title,
30
+ description: doc.description || "",
31
+ url: `/docs/${doc.slug}`,
32
+ // Extract a snippet from the content
33
+ snippet: extractSnippet(doc.content, query),
34
+ }))
35
+ .slice(0, 10) // Limit results
36
+
37
+ return NextResponse.json({ results })
38
+ } catch (error) {
39
+ console.error("Search error:", error)
40
+ return NextResponse.json({ results: [], error: "Search failed" }, { status: 500 })
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Extract a snippet around the search query
46
+ */
47
+ function extractSnippet(content: string, query: string): string {
48
+ const lowerContent = content.toLowerCase()
49
+ const index = lowerContent.indexOf(query)
50
+
51
+ if (index === -1) {
52
+ // Return first 150 chars if query not found in content
53
+ return `${content.slice(0, 150).trim()}...`
54
+ }
55
+
56
+ // Get 50 chars before and 100 chars after the match
57
+ const start = Math.max(0, index - 50)
58
+ const end = Math.min(content.length, index + query.length + 100)
59
+
60
+ let snippet = content.slice(start, end).trim()
61
+
62
+ // Add ellipsis if needed
63
+ if (start > 0) snippet = `...${snippet}`
64
+ if (end < content.length) snippet = `${snippet}...`
65
+
66
+ return snippet
67
+ }
@@ -0,0 +1,260 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ const RESEND_API_URL = "https://api.resend.com"
4
+
5
+ /**
6
+ * Email validation regex
7
+ */
8
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
9
+
10
+ /**
11
+ * Company address for CAN-SPAM compliance
12
+ */
13
+ const COMPANY_ADDRESS = "KrumaLabs • 102 West Main Street #501, New Albany, OH 43054"
14
+
15
+ /**
16
+ * DirectoryHub logo URL
17
+ */
18
+ const LOGO_URL = "https://directoryhub.app/logo.png"
19
+
20
+ /**
21
+ * Generate newsletter confirmation email HTML with logo and branding
22
+ */
23
+ function renderNewsletterConfirmationEmail(unsubscribeUrl?: string): string {
24
+ const emailStyles = {
25
+ main: `
26
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
27
+ line-height: 1.6;
28
+ color: #333;
29
+ background-color: #f5f5f5;
30
+ margin: 0;
31
+ padding: 20px;
32
+ `,
33
+ container: `
34
+ background-color: #ffffff;
35
+ border-radius: 8px;
36
+ margin: 0 auto;
37
+ padding: 0;
38
+ max-width: 600px;
39
+ `,
40
+ header: `
41
+ padding: 32px 32px 24px;
42
+ border-bottom: 1px solid #e5e5e5;
43
+ `,
44
+ logoText: `
45
+ color: #0070f3;
46
+ font-size: 24px;
47
+ font-weight: 600;
48
+ margin: 0;
49
+ padding: 0;
50
+ line-height: 1;
51
+ `,
52
+ heading: `
53
+ color: #111111;
54
+ font-size: 24px;
55
+ font-weight: 600;
56
+ line-height: 1.4;
57
+ margin: 0 0 24px;
58
+ `,
59
+ content: `
60
+ padding: 32px;
61
+ `,
62
+ paragraph: `
63
+ font-size: 16px;
64
+ color: #444444;
65
+ margin: 0 0 16px;
66
+ line-height: 1.6;
67
+ `,
68
+ paragraphSmall: `
69
+ font-size: 14px;
70
+ color: #666666;
71
+ margin: 0 0 16px;
72
+ line-height: 1.6;
73
+ `,
74
+ button: `
75
+ background-color: #0070f3;
76
+ color: white;
77
+ padding: 14px 32px;
78
+ text-decoration: none;
79
+ border-radius: 6px;
80
+ display: inline-block;
81
+ font-weight: 500;
82
+ font-size: 16px;
83
+ `,
84
+ footer: `
85
+ padding: 24px 32px;
86
+ text-align: center;
87
+ `,
88
+ footerText: `
89
+ color: #666666;
90
+ font-size: 14px;
91
+ line-height: 1.6;
92
+ margin: 0 0 8px;
93
+ `,
94
+ footerTextSmall: `
95
+ color: #999999;
96
+ font-size: 12px;
97
+ line-height: 1.6;
98
+ margin: 16px 0 0;
99
+ `,
100
+ footerAddress: `
101
+ color: #999999;
102
+ font-size: 11px;
103
+ line-height: 1.6;
104
+ margin: 8px 0 0;
105
+ `,
106
+ link: `
107
+ color: #0070f3;
108
+ text-decoration: underline;
109
+ `,
110
+ }
111
+
112
+ return `
113
+ <!DOCTYPE html>
114
+ <html lang="en">
115
+ <head>
116
+ <meta charset="utf-8">
117
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
118
+ </head>
119
+ <body style="${emailStyles.main}">
120
+ <div style="${emailStyles.container}">
121
+ <!-- Header with Logo -->
122
+ <div style="${emailStyles.header}">
123
+ <a href="https://directoryhub.app" style="display: inline-flex; align-items: center; gap: 12px; text-decoration: none; color: inherit;">
124
+ <img src="${LOGO_URL}" alt="DirectoryHub" width="32" height="32" style="display: block; width: 32px; height: 32px;">
125
+ <span style="${emailStyles.logoText}">DirectoryHub</span>
126
+ </a>
127
+ </div>
128
+
129
+ <!-- Content -->
130
+ <div style="padding: 32px 32px 0;"><h1 style="${emailStyles.heading}">Welcome to the Newsletter!</h1></div>
131
+ <div style="${emailStyles.content}">
132
+ <p style="${emailStyles.paragraph}">
133
+ Thanks for subscribing to the DirectoryHub newsletter! 🎉
134
+ </p>
135
+ <p style="${emailStyles.paragraph}">
136
+ You'll now receive updates about:
137
+ </p>
138
+ <ul style="${emailStyles.paragraph}">
139
+ <li>New features and product updates</li>
140
+ <li>Tips for building better directories</li>
141
+ <li>Industry insights and best practices</li>
142
+ <li>Special announcements and offers</li>
143
+ </ul>
144
+ <p style="${emailStyles.paragraph}">
145
+ We respect your inbox and only send emails when we have something valuable to share.
146
+ </p>
147
+ <div style="margin: 32px 0; text-align: center;">
148
+ <a href="https://directoryhub.app" style="${emailStyles.button}">Visit DirectoryHub</a>
149
+ </div>
150
+ <p style="${emailStyles.paragraphSmall}">
151
+ If you didn't subscribe to this newsletter, you can safely ignore this email or
152
+ <a href="${unsubscribeUrl || "https://directoryhub.app/unsubscribe"}" style="${emailStyles.link}">unsubscribe here</a>.
153
+ </p>
154
+ </div>
155
+
156
+ <!-- Divider -->
157
+ <hr style="border: none; border-top: 1px solid #e5e5e5; margin: 0 32px;">
158
+
159
+ <!-- Footer -->
160
+ <div style="${emailStyles.footer}">
161
+ <p style="${emailStyles.footerText}">DirectoryHub - Build beautiful directory websites</p>
162
+ <p style="${emailStyles.footerText}">
163
+ <a href="https://directoryhub.app" style="${emailStyles.link}">Visit our website</a>
164
+ <span style="color: #999999;"> • </span>
165
+ <a href="https://directoryhub.app/support" style="${emailStyles.link}">Support</a>
166
+ ${unsubscribeUrl ? `<span style="color: #999999;"> • </span><a href="${unsubscribeUrl}" style="${emailStyles.link}">Unsubscribe</a>` : ""}
167
+ </p>
168
+ <p style="${emailStyles.footerTextSmall}">You're receiving this email because you signed up for the DirectoryHub newsletter.</p>
169
+ <p style="${emailStyles.footerAddress}">${COMPANY_ADDRESS}</p>
170
+ </div>
171
+ </div>
172
+ </body>
173
+ </html>`.trim()
174
+ }
175
+
176
+ export async function POST(request: Request) {
177
+ try {
178
+ const body = await request.json()
179
+ const { email } = body
180
+
181
+ if (!email) {
182
+ return NextResponse.json({ success: false, message: "Email is required." }, { status: 400 })
183
+ }
184
+
185
+ // Normalize and validate email
186
+ const normalizedEmail = email.toLowerCase().trim()
187
+ if (!EMAIL_REGEX.test(normalizedEmail)) {
188
+ return NextResponse.json(
189
+ { success: false, message: "Please enter a valid email address." },
190
+ { status: 400 },
191
+ )
192
+ }
193
+
194
+ const apiKey = process.env.RESEND_API_KEY
195
+ const audienceId = process.env.RESEND_AUDIENCE_NEWSLETTER
196
+ // Marketing emails use "Theo from DirectoryHub" as sender
197
+ const fromEmail = "Theo from DirectoryHub <theo@notifications.directoryhub.app>"
198
+
199
+ // In development without API key, just return success
200
+ if (!apiKey) {
201
+ console.warn("RESEND_API_KEY not set - newsletter signup simulated")
202
+ return NextResponse.json({
203
+ success: true,
204
+ message: "Thanks for subscribing! Check your inbox for a confirmation email.",
205
+ })
206
+ }
207
+
208
+ // Call Convex newsletter subscribe action via HTTP
209
+ // This ensures consistent handling and uses the proper addNewsletterSubscriber function
210
+ const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL || process.env.CONVEX_URL
211
+ if (!convexUrl) {
212
+ console.warn("CONVEX_URL not set - newsletter signup will fail")
213
+ return NextResponse.json(
214
+ { success: false, message: "Server configuration error. Please try again later." },
215
+ { status: 500 },
216
+ )
217
+ }
218
+
219
+ try {
220
+ const response = await fetch(`${convexUrl}/api/newsletter/subscribe`, {
221
+ method: "POST",
222
+ headers: {
223
+ "Content-Type": "application/json",
224
+ },
225
+ body: JSON.stringify({
226
+ email: normalizedEmail,
227
+ }),
228
+ })
229
+
230
+ const result = await response.json()
231
+
232
+ if (!result.success) {
233
+ return NextResponse.json(result, { status: response.status })
234
+ }
235
+
236
+ return NextResponse.json(result)
237
+ } catch (error) {
238
+ console.error("Newsletter subscription error:", error)
239
+ return NextResponse.json(
240
+ { success: false, message: "Something went wrong. Please try again later." },
241
+ { status: 500 },
242
+ )
243
+ }
244
+ } catch (error) {
245
+ console.error("Newsletter subscription error:", error)
246
+
247
+ // Check if it's a duplicate subscriber error
248
+ if (error instanceof Error && error.message.includes("already exists")) {
249
+ return NextResponse.json({
250
+ success: true,
251
+ message: "You're already subscribed to our newsletter!",
252
+ })
253
+ }
254
+
255
+ return NextResponse.json(
256
+ { success: false, message: "Something went wrong. Please try again later." },
257
+ { status: 500 },
258
+ )
259
+ }
260
+ }
@@ -0,0 +1,266 @@
1
+ import { NextResponse } from "next/server"
2
+ import Stripe from "stripe"
3
+
4
+ /**
5
+ * Stripe Lookup Keys - defined in Stripe Dashboard for each price
6
+ * These must match the lookup keys configured in your Stripe Dashboard
7
+ */
8
+ const STRIPE_LOOKUP_KEYS = {
9
+ free_monthly: "free_monthly",
10
+ free_annual: "free_annual",
11
+ pro_monthly: "pro_monthly",
12
+ pro_annual: "pro_annual",
13
+ business_monthly: "business_monthly",
14
+ business_annual: "business_annual",
15
+ } as const
16
+
17
+ /**
18
+ * Default product metadata when Stripe metadata is missing
19
+ */
20
+ const DEFAULT_PRODUCT_INFO: Record<
21
+ string,
22
+ { description: string; popular: boolean; order: number }
23
+ > = {
24
+ free: { description: "Perfect for getting started", popular: false, order: 1 },
25
+ pro: { description: "For growing businesses", popular: false, order: 2 },
26
+ business: { description: "For teams and agencies", popular: true, order: 3 },
27
+ }
28
+
29
+ /**
30
+ * Feature type from Stripe Entitlements
31
+ */
32
+ interface PlanFeature {
33
+ id: string
34
+ lookupKey: string
35
+ name: string
36
+ metadata?: Record<string, string>
37
+ }
38
+
39
+ /**
40
+ * All features type for comparison table
41
+ */
42
+ interface AllFeature {
43
+ id: string
44
+ lookupKey: string
45
+ name: string
46
+ category?: string
47
+ metadata?: Record<string, string>
48
+ }
49
+
50
+ /**
51
+ * Plan pricing type with features from Stripe
52
+ */
53
+ interface PlanPricing {
54
+ planId: string
55
+ stripeProductId: string
56
+ name: string
57
+ description: string
58
+ features: PlanFeature[]
59
+ monthlyPriceId?: string
60
+ monthlyAmount: number
61
+ annualPriceId?: string
62
+ annualAmount: number
63
+ annualDiscount: number
64
+ popular: boolean
65
+ order: number
66
+ }
67
+
68
+ /**
69
+ * Fetch pricing and features from Stripe and return formatted data
70
+ * GET /api/pricing
71
+ *
72
+ * Returns JSON with plan pricing and features for the marketing site
73
+ */
74
+ export async function GET() {
75
+ try {
76
+ const stripeSecretKey = process.env.STRIPE_SECRET_KEY
77
+
78
+ if (!stripeSecretKey) {
79
+ return NextResponse.json(
80
+ {
81
+ plans: [],
82
+ allFeatures: [],
83
+ lastFetched: Date.now(),
84
+ error: "Stripe not configured",
85
+ },
86
+ { status: 200 },
87
+ )
88
+ }
89
+
90
+ const stripe = new Stripe(stripeSecretKey)
91
+
92
+ // Fetch all features for comparison table
93
+ const allFeatures: AllFeature[] = []
94
+ try {
95
+ const featuresResponse = await stripe.entitlements.features.list({ limit: 100 })
96
+ for (const feature of featuresResponse.data) {
97
+ allFeatures.push({
98
+ id: feature.id,
99
+ lookupKey: feature.lookup_key,
100
+ name: feature.name,
101
+ category: feature.metadata?.category,
102
+ metadata: feature.metadata as Record<string, string> | undefined,
103
+ })
104
+ }
105
+ } catch (err) {
106
+ console.error("Error fetching Stripe features:", err)
107
+ }
108
+
109
+ // Fetch prices with product info
110
+ const priceMap = new Map<string, { id: string; amount: number; productId: string }>()
111
+
112
+ try {
113
+ const allLookupKeys = Object.values(STRIPE_LOOKUP_KEYS)
114
+ const prices = await stripe.prices.list({
115
+ lookup_keys: allLookupKeys,
116
+ active: true,
117
+ expand: ["data.product"],
118
+ limit: 20,
119
+ })
120
+
121
+ for (const price of prices.data) {
122
+ if (price.lookup_key && price.unit_amount !== null) {
123
+ const productId = typeof price.product === "string" ? price.product : price.product?.id
124
+ if (productId) {
125
+ priceMap.set(price.lookup_key, {
126
+ id: price.id,
127
+ amount: price.unit_amount,
128
+ productId,
129
+ })
130
+ }
131
+ }
132
+ }
133
+ } catch (stripeError) {
134
+ console.error("Error fetching Stripe prices:", stripeError)
135
+ }
136
+
137
+ // Group prices by product
138
+ const productPrices = new Map<
139
+ string,
140
+ { monthly?: { id: string; amount: number }; annual?: { id: string; amount: number } }
141
+ >()
142
+
143
+ for (const [lookupKey, priceData] of priceMap.entries()) {
144
+ const isAnnual = lookupKey.endsWith("_annual")
145
+ const existingPrices = productPrices.get(priceData.productId) || {}
146
+
147
+ if (isAnnual) {
148
+ existingPrices.annual = { id: priceData.id, amount: priceData.amount }
149
+ } else {
150
+ existingPrices.monthly = { id: priceData.id, amount: priceData.amount }
151
+ }
152
+
153
+ productPrices.set(priceData.productId, existingPrices)
154
+ }
155
+
156
+ // Build plans from products
157
+ const plans: PlanPricing[] = []
158
+
159
+ for (const [productId, prices] of productPrices.entries()) {
160
+ // Fetch product details
161
+ let product: Stripe.Product | null = null
162
+ try {
163
+ const fetched = await stripe.products.retrieve(productId)
164
+ if (!fetched.deleted) {
165
+ product = fetched
166
+ }
167
+ } catch (err) {
168
+ console.error(`Error fetching product ${productId}:`, err)
169
+ continue
170
+ }
171
+
172
+ if (!product) continue
173
+
174
+ // Get plan ID from product metadata
175
+ const planId = product.metadata?.plan_id
176
+ if (!planId) {
177
+ console.warn(`Product ${productId} missing plan_id metadata, skipping`)
178
+ continue
179
+ }
180
+
181
+ // Fetch features for this product
182
+ const features: PlanFeature[] = []
183
+ try {
184
+ const productFeatures = await stripe.products.listFeatures(productId, {
185
+ limit: 100,
186
+ })
187
+ for (const pf of productFeatures.data) {
188
+ features.push({
189
+ id: pf.entitlement_feature.id,
190
+ lookupKey: pf.entitlement_feature.lookup_key,
191
+ name: pf.entitlement_feature.name,
192
+ metadata: pf.entitlement_feature.metadata as Record<string, string> | undefined,
193
+ })
194
+ }
195
+ } catch (err) {
196
+ console.error(`Error fetching features for product ${productId}:`, err)
197
+ }
198
+
199
+ // Get metadata or use defaults
200
+ const defaultInfo = DEFAULT_PRODUCT_INFO[planId] || {
201
+ description: "",
202
+ popular: false,
203
+ order: 99,
204
+ }
205
+ const description =
206
+ product.metadata?.description || product.description || defaultInfo.description
207
+ const popular = product.metadata?.popular === "true" || defaultInfo.popular
208
+ const order = Number.parseInt(product.metadata?.order || String(defaultInfo.order), 10)
209
+
210
+ const monthlyAmount = prices.monthly?.amount ?? 0
211
+ const annualAmount = prices.annual?.amount ?? 0
212
+
213
+ // Calculate annual discount
214
+ const yearlyMonthlyEquivalent = monthlyAmount * 12
215
+ const annualDiscount =
216
+ yearlyMonthlyEquivalent > 0
217
+ ? Math.round(((yearlyMonthlyEquivalent - annualAmount) / yearlyMonthlyEquivalent) * 100)
218
+ : 0
219
+
220
+ plans.push({
221
+ planId,
222
+ stripeProductId: productId,
223
+ name: product.name,
224
+ description,
225
+ features,
226
+ monthlyPriceId: prices.monthly?.id,
227
+ monthlyAmount,
228
+ annualPriceId: prices.annual?.id,
229
+ annualAmount,
230
+ annualDiscount: Math.max(0, annualDiscount),
231
+ popular,
232
+ order,
233
+ })
234
+ }
235
+
236
+ // Sort plans by order
237
+ plans.sort((a, b) => a.order - b.order)
238
+
239
+ return NextResponse.json(
240
+ {
241
+ plans,
242
+ allFeatures,
243
+ lastFetched: Date.now(),
244
+ },
245
+ {
246
+ status: 200,
247
+ headers: {
248
+ // Cache for 1 hour on CDN, revalidate in background
249
+ "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200",
250
+ },
251
+ },
252
+ )
253
+ } catch (error) {
254
+ console.error("Pricing API error:", error)
255
+
256
+ return NextResponse.json(
257
+ {
258
+ plans: [],
259
+ allFeatures: [],
260
+ lastFetched: Date.now(),
261
+ error: "Failed to fetch pricing",
262
+ },
263
+ { status: 200 },
264
+ )
265
+ }
266
+ }