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.
- package/.turbo/turbo-build.log +6 -6
- package/dist/index.js +8057 -440
- package/package.json +1 -1
- package/src/templates.generated.ts +248 -94
- package/templates/integrations/posthog/src/components/providers/posthog-provider.tsx.hbs +4 -1
- package/templates/marketing/payload/package.json.hbs +41 -26
- package/templates/marketing/payload/src/Footer/Component.client.tsx +288 -0
- package/templates/marketing/payload/src/Footer/Component.tsx +11 -0
- package/templates/marketing/payload/src/Footer/RowLabel.tsx +15 -0
- package/templates/marketing/payload/src/Footer/config.ts +178 -0
- package/templates/marketing/payload/src/Footer/hooks/{revalidateFooter.ts.hbs → revalidateFooter.ts} +5 -5
- package/templates/marketing/payload/src/Header/Component.client.tsx +94 -0
- package/templates/marketing/payload/src/Header/Component.tsx +10 -0
- package/templates/marketing/payload/src/Header/MegaMenu/index.tsx +197 -0
- package/templates/marketing/payload/src/Header/MobileMenu/HamburgerIcon.tsx +48 -0
- package/templates/marketing/payload/src/Header/MobileMenu/index.tsx +299 -0
- package/templates/marketing/payload/src/Header/Nav/index.tsx +76 -0
- package/templates/marketing/payload/src/Header/RowLabel.tsx +21 -0
- package/templates/marketing/payload/src/Header/config.ts +208 -0
- package/templates/marketing/payload/src/Header/hooks/{revalidateHeader.ts.hbs → revalidateHeader.ts} +5 -5
- package/templates/marketing/payload/src/access/{authenticated.ts.hbs → authenticated.ts} +1 -1
- package/templates/marketing/payload/src/access/{authenticatedOrPublished.ts.hbs → authenticatedOrPublished.ts} +8 -8
- package/templates/marketing/payload/src/app/(docs)/docs/[[...slug]]/page.tsx +117 -0
- package/templates/marketing/payload/src/app/(docs)/docs/layout.tsx +39 -0
- package/templates/marketing/payload/src/app/(docs)/layout.tsx +44 -0
- package/templates/marketing/payload/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts +68 -0
- package/templates/marketing/payload/src/app/(frontend)/(sitemaps)/posts-sitemap.xml/route.ts +55 -0
- package/templates/marketing/payload/src/app/(frontend)/[slug]/page.client.tsx +15 -0
- package/templates/marketing/payload/src/app/(frontend)/[slug]/page.tsx +114 -0
- package/templates/marketing/payload/src/app/(frontend)/api/docs-search/route.ts +67 -0
- package/templates/marketing/payload/src/app/(frontend)/api/newsletter/route.ts +260 -0
- package/templates/marketing/payload/src/app/(frontend)/api/pricing/route.ts +266 -0
- package/templates/marketing/payload/src/app/(frontend)/globals.css +1019 -0
- package/templates/marketing/payload/src/app/(frontend)/layout.tsx +114 -0
- package/templates/marketing/payload/src/app/(frontend)/next/exit-preview/route.ts +7 -0
- package/templates/marketing/payload/src/app/(frontend)/next/preview/route.ts +56 -0
- package/templates/marketing/payload/src/app/(frontend)/next/seed/route.ts +31 -0
- package/templates/marketing/payload/src/app/(frontend)/not-found.tsx +17 -0
- package/templates/marketing/payload/src/app/(frontend)/page.tsx +5 -0
- package/templates/marketing/payload/src/app/(frontend)/posts/BlogPageClient.tsx +190 -0
- package/templates/marketing/payload/src/app/(frontend)/posts/[slug]/BlogPostContent.tsx +67 -0
- package/templates/marketing/payload/src/app/(frontend)/posts/[slug]/page.client.tsx +15 -0
- package/templates/marketing/payload/src/app/(frontend)/posts/[slug]/page.tsx +118 -0
- package/templates/marketing/payload/src/app/(frontend)/posts/page/[pageNumber]/page.client.tsx +15 -0
- package/templates/marketing/payload/src/app/(frontend)/posts/page/[pageNumber]/page.tsx +87 -0
- package/templates/marketing/payload/src/app/(frontend)/posts/page.tsx +49 -0
- package/templates/marketing/payload/src/app/(frontend)/search/page.client.tsx +15 -0
- package/templates/marketing/payload/src/app/(frontend)/search/page.tsx +87 -0
- package/templates/marketing/payload/src/app/(payload)/admin/[[...segments]]/not-found.tsx +24 -0
- package/templates/marketing/payload/src/app/(payload)/admin/[[...segments]]/page.tsx +24 -0
- package/templates/marketing/payload/src/app/(payload)/admin/importMap.js +83 -0
- package/templates/marketing/payload/src/app/(payload)/api/[...slug]/{route.ts.hbs → route.ts} +13 -9
- package/templates/marketing/payload/src/app/(payload)/api/graphql/{route.ts.hbs → route.ts} +2 -2
- package/templates/marketing/payload/src/app/(payload)/api/graphql-playground/{route.ts.hbs → route.ts} +3 -3
- package/templates/marketing/payload/src/app/(payload)/custom.scss +0 -0
- package/templates/marketing/payload/src/app/(payload)/layout.tsx +31 -0
- package/templates/marketing/payload/src/blocks/ArchiveBlock/Component.tsx +65 -0
- package/templates/marketing/payload/src/blocks/ArchiveBlock/config.ts +120 -0
- package/templates/marketing/payload/src/blocks/Banner/Component.tsx +26 -0
- package/templates/marketing/payload/src/blocks/Banner/config.ts +67 -0
- package/templates/marketing/payload/src/blocks/BentoFeatures/Component.tsx +243 -0
- package/templates/marketing/payload/src/blocks/BentoFeatures/config.ts +147 -0
- package/templates/marketing/payload/src/blocks/CallToAction/Component.tsx +31 -0
- package/templates/marketing/payload/src/blocks/CallToAction/config.ts +68 -0
- package/templates/marketing/payload/src/blocks/Code/Component.client.tsx +33 -0
- package/templates/marketing/payload/src/blocks/Code/Component.tsx +21 -0
- package/templates/marketing/payload/src/blocks/Code/CopyButton.tsx +33 -0
- package/templates/marketing/payload/src/blocks/Code/config.ts +33 -0
- package/templates/marketing/payload/src/blocks/Content/Component.tsx +41 -0
- package/templates/marketing/payload/src/blocks/Content/config.ts +105 -0
- package/templates/marketing/payload/src/blocks/FAQAccordion/Component.tsx +90 -0
- package/templates/marketing/payload/src/blocks/FAQAccordion/config.ts +75 -0
- package/templates/marketing/payload/src/blocks/FeatureGrid/Component.tsx +124 -0
- package/templates/marketing/payload/src/blocks/FeatureGrid/config.ts +120 -0
- package/templates/marketing/payload/src/blocks/FeatureShowcase/Component.tsx +107 -0
- package/templates/marketing/payload/src/blocks/FeatureShowcase/config.ts +111 -0
- package/templates/marketing/payload/src/blocks/FinalCTA/Component.tsx +117 -0
- package/templates/marketing/payload/src/blocks/FinalCTA/config.ts +50 -0
- package/templates/marketing/payload/src/blocks/Form/Checkbox/index.tsx +45 -0
- package/templates/marketing/payload/src/blocks/Form/Component.tsx +170 -0
- package/templates/marketing/payload/src/blocks/Form/Country/index.tsx +65 -0
- package/templates/marketing/payload/src/blocks/Form/Country/options.ts +982 -0
- package/templates/marketing/payload/src/blocks/Form/Email/index.tsx +38 -0
- package/templates/marketing/payload/src/blocks/Form/Error/index.tsx +13 -0
- package/templates/marketing/payload/src/blocks/Form/Message/index.tsx +13 -0
- package/templates/marketing/payload/src/blocks/Form/Number/index.tsx +36 -0
- package/templates/marketing/payload/src/blocks/Form/Select/index.tsx +63 -0
- package/templates/marketing/payload/src/blocks/Form/State/index.tsx +64 -0
- package/templates/marketing/payload/src/blocks/Form/State/options.ts +52 -0
- package/templates/marketing/payload/src/blocks/Form/Text/index.tsx +32 -0
- package/templates/marketing/payload/src/blocks/Form/Textarea/index.tsx +40 -0
- package/templates/marketing/payload/src/blocks/Form/Width/index.tsx +13 -0
- package/templates/marketing/payload/src/blocks/Form/config.ts +77 -0
- package/templates/marketing/payload/src/blocks/Form/fields.tsx +21 -0
- package/templates/marketing/payload/src/blocks/HowItWorks/Component.tsx +59 -0
- package/templates/marketing/payload/src/blocks/HowItWorks/config.ts +88 -0
- package/templates/marketing/payload/src/blocks/IndustryTabs/Component.tsx +132 -0
- package/templates/marketing/payload/src/blocks/IndustryTabs/config.ts +77 -0
- package/templates/marketing/payload/src/blocks/LogoBanner/Component.tsx +95 -0
- package/templates/marketing/payload/src/blocks/LogoBanner/config.ts +48 -0
- package/templates/marketing/payload/src/blocks/MediaBlock/Component.tsx +67 -0
- package/templates/marketing/payload/src/blocks/MediaBlock/config.ts +14 -0
- package/templates/marketing/payload/src/blocks/Personas/Component.tsx +69 -0
- package/templates/marketing/payload/src/blocks/Personas/config.ts +96 -0
- package/templates/marketing/payload/src/blocks/PricingTable/ComparisonTable.tsx +250 -0
- package/templates/marketing/payload/src/blocks/PricingTable/Component.tsx +443 -0
- package/templates/marketing/payload/src/blocks/PricingTable/config.ts +142 -0
- package/templates/marketing/payload/src/blocks/ProofBanner/Component.tsx +65 -0
- package/templates/marketing/payload/src/blocks/ProofBanner/config.ts +42 -0
- package/templates/marketing/payload/src/blocks/RelatedPosts/Component.tsx +32 -0
- package/templates/marketing/payload/src/blocks/RenderBlocks.tsx +92 -0
- package/templates/marketing/payload/src/blocks/TestimonialsGrid/Component.tsx +107 -0
- package/templates/marketing/payload/src/blocks/TestimonialsGrid/config.ts +76 -0
- package/templates/marketing/payload/src/blocks/TrustColumns/Component.tsx +83 -0
- package/templates/marketing/payload/src/blocks/TrustColumns/config.ts +70 -0
- package/templates/marketing/payload/src/collections/Categories.ts +28 -0
- package/templates/marketing/payload/src/collections/FAQs/index.ts +100 -0
- package/templates/marketing/payload/src/collections/Media.ts +160 -0
- package/templates/marketing/payload/src/collections/Pages/hooks/revalidatePage.ts +43 -0
- package/templates/marketing/payload/src/collections/Pages/index.ts +168 -0
- package/templates/marketing/payload/src/collections/Posts/hooks/populateAuthors.ts +41 -0
- package/templates/marketing/payload/src/collections/Posts/hooks/revalidatePost.ts +44 -0
- package/templates/marketing/payload/src/collections/Posts/index.ts +259 -0
- package/templates/marketing/payload/src/collections/Users/index.ts +26 -0
- package/templates/marketing/payload/src/components/AdminBar/index.scss +7 -0
- package/templates/marketing/payload/src/components/AdminBar/index.tsx +89 -0
- package/templates/marketing/payload/src/components/Analytics/CTATracker.tsx +33 -0
- package/templates/marketing/payload/src/components/Analytics/FeatureSectionTracker.tsx +47 -0
- package/templates/marketing/payload/src/components/Analytics/PricingViewTracker.tsx +46 -0
- package/templates/marketing/payload/src/components/Analytics/index.tsx +3 -0
- package/templates/marketing/payload/src/components/BeforeDashboard/SeedButton/index.tsx +89 -0
- package/templates/marketing/payload/src/components/BeforeDashboard/index.tsx +69 -0
- package/templates/marketing/payload/src/components/BeforeLogin/index.tsx +14 -0
- package/templates/marketing/payload/src/components/BlogCTA/index.tsx +77 -0
- package/templates/marketing/payload/src/components/Card/index.tsx +85 -0
- package/templates/marketing/payload/src/components/CollectionArchive/index.tsx +32 -0
- package/templates/marketing/payload/src/components/JsonLd/index.tsx +138 -0
- package/templates/marketing/payload/src/components/Link/index.tsx +66 -0
- package/templates/marketing/payload/src/components/LivePreviewListener/index.tsx +10 -0
- package/templates/marketing/payload/src/components/Logo/Logo.tsx +46 -0
- package/templates/marketing/payload/src/components/Media/ImageMedia/index.tsx +80 -0
- package/templates/marketing/payload/src/components/Media/VideoMedia/index.tsx +47 -0
- package/templates/marketing/payload/src/components/Media/index.tsx +26 -0
- package/templates/marketing/payload/src/components/Media/types.ts +22 -0
- package/templates/marketing/payload/src/components/PageRange/index.tsx +57 -0
- package/templates/marketing/payload/src/components/Pagination/index.tsx +101 -0
- package/templates/marketing/payload/src/components/PayloadRedirects/index.tsx +48 -0
- package/templates/marketing/payload/src/components/RichText/index.tsx +152 -0
- package/templates/marketing/payload/src/components/TableOfContents/index.tsx +128 -0
- package/templates/marketing/payload/src/components/ui/accordion.tsx +64 -0
- package/templates/marketing/payload/src/components/ui/button.tsx +52 -0
- package/templates/marketing/payload/src/components/ui/card.tsx +48 -0
- package/templates/marketing/payload/src/components/ui/checkbox.tsx +27 -0
- package/templates/marketing/payload/src/components/ui/input.tsx +22 -0
- package/templates/marketing/payload/src/components/ui/label.tsx +19 -0
- package/templates/marketing/payload/src/components/ui/pagination.tsx +92 -0
- package/templates/marketing/payload/src/components/ui/select.tsx +144 -0
- package/templates/marketing/payload/src/components/ui/textarea.tsx +21 -0
- package/templates/marketing/payload/src/endpoints/seed/contact-form.ts +111 -0
- package/templates/marketing/payload/src/endpoints/seed/contact-page.ts +56 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/about.ts +281 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/faqs.ts +224 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/automation.ts +229 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/custom-fields.ts +229 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/dashboard.ts +228 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/index.ts +6 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/monetization.ts +230 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/seo.ts +229 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/features/templates.ts +218 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/home.ts +555 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/index.ts +767 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/posts.ts +623 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/pricing.ts +251 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/privacy.ts +457 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/richtext-helper.ts +88 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/terms.ts +478 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/b2b-vendor-hubs.ts +229 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/communities.ts +230 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/index.ts +4 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/local-services.ts +230 -0
- package/templates/marketing/payload/src/endpoints/seed/directoryhub/use-cases/marketplaces.ts +230 -0
- package/templates/marketing/payload/src/endpoints/seed/home-static.ts +691 -0
- package/templates/marketing/payload/src/endpoints/seed/home.ts +675 -0
- package/templates/marketing/payload/src/endpoints/seed/image-1.ts +67 -0
- package/templates/marketing/payload/src/endpoints/seed/image-2.ts +67 -0
- package/templates/marketing/payload/src/endpoints/seed/image-3.ts +67 -0
- package/templates/marketing/payload/src/endpoints/seed/image-hero-1.ts +5 -0
- package/templates/marketing/payload/src/endpoints/seed/image-hero1.webp +0 -0
- package/templates/marketing/payload/src/endpoints/seed/image-post1.webp +0 -0
- package/templates/marketing/payload/src/endpoints/seed/image-post2.webp +0 -0
- package/templates/marketing/payload/src/endpoints/seed/image-post3.webp +0 -0
- package/templates/marketing/payload/src/endpoints/seed/index.ts +335 -0
- package/templates/marketing/payload/src/endpoints/seed/post-1.ts +315 -0
- package/templates/marketing/payload/src/endpoints/seed/post-2.ts +232 -0
- package/templates/marketing/payload/src/endpoints/seed/post-3.ts +268 -0
- package/templates/marketing/payload/src/fields/defaultLexical.ts +73 -0
- package/templates/marketing/payload/src/fields/link.ts +139 -0
- package/templates/marketing/payload/src/fields/linkGroup.ts +28 -0
- package/templates/marketing/payload/src/heros/HighImpact/index.tsx +56 -0
- package/templates/marketing/payload/src/heros/LowImpact/index.tsx +48 -0
- package/templates/marketing/payload/src/heros/MediumImpact/index.tsx +50 -0
- package/templates/marketing/payload/src/heros/PostHero/index.tsx +73 -0
- package/templates/marketing/payload/src/heros/ProductShowcase/AnimatedMockup.tsx +241 -0
- package/templates/marketing/payload/src/heros/ProductShowcase/index.tsx +108 -0
- package/templates/marketing/payload/src/heros/{RenderHero.tsx.hbs → RenderHero.tsx} +9 -9
- package/templates/marketing/payload/src/heros/config.ts +121 -0
- package/templates/marketing/payload/src/hooks/populatePublishedAt.ts +15 -0
- package/templates/marketing/payload/src/hooks/{revalidateRedirects.ts.hbs → revalidateRedirects.ts} +3 -3
- package/templates/marketing/payload/src/lib/convex.ts +13 -0
- package/templates/marketing/payload/src/lib/docs-source.ts +138 -0
- package/templates/marketing/payload/src/lib/mdx.tsx +191 -0
- package/templates/marketing/payload/src/payload.config.ts.hbs +95 -145
- package/templates/marketing/payload/src/plugins/index.ts +107 -0
- package/templates/marketing/payload/src/providers/HeaderTheme/index.tsx +34 -0
- package/templates/marketing/payload/src/providers/PostHogProvider.tsx +33 -0
- package/templates/marketing/payload/src/providers/Theme/InitTheme/{index.tsx.hbs → index.tsx} +11 -10
- package/templates/marketing/payload/src/providers/Theme/ThemeSelector/index.tsx +133 -0
- package/templates/marketing/payload/src/providers/Theme/ThemeSelector/types.ts +7 -0
- package/templates/marketing/payload/src/providers/Theme/index.tsx +60 -0
- package/templates/marketing/payload/src/providers/Theme/shared.ts +17 -0
- package/templates/marketing/payload/src/providers/Theme/{types.ts.hbs → types.ts} +3 -3
- package/templates/marketing/payload/src/providers/index.tsx +17 -0
- package/templates/marketing/payload/src/search/Component.tsx +42 -0
- package/templates/marketing/payload/src/search/beforeSync.ts +56 -0
- package/templates/marketing/payload/src/search/fieldOverrides.ts +61 -0
- package/templates/marketing/payload/src/utilities/deepMerge.ts +35 -0
- package/templates/marketing/payload/src/utilities/extractHeadings.ts +78 -0
- package/templates/marketing/payload/src/utilities/formatAuthors.ts +24 -0
- package/templates/marketing/payload/src/utilities/formatDateTime.ts +20 -0
- package/templates/marketing/payload/src/utilities/generateMeta.ts +93 -0
- package/templates/marketing/payload/src/utilities/generatePreviewPath.ts +33 -0
- package/templates/marketing/payload/src/utilities/getDocument.ts +32 -0
- package/templates/marketing/payload/src/utilities/getGlobals.ts +26 -0
- package/templates/marketing/payload/src/utilities/getMeUser.ts +43 -0
- package/templates/marketing/payload/src/utilities/getMediaUrl.ts +24 -0
- package/templates/marketing/payload/src/utilities/getRedirects.ts +26 -0
- package/templates/marketing/payload/src/utilities/getURL.ts +26 -0
- package/templates/marketing/payload/src/utilities/mergeOpenGraph.ts +26 -0
- package/templates/marketing/payload/src/utilities/toKebabCase.ts +5 -0
- package/templates/marketing/payload/src/utilities/ui.ts +12 -0
- package/templates/marketing/payload/src/utilities/useClickableCard.ts +108 -0
- package/templates/marketing/payload/src/utilities/useDebounce.ts +17 -0
- package/templates/packages/ui/src/components/button.tsx.hbs +53 -0
- package/templates/packages/ui/src/components/card.tsx.hbs +76 -0
- package/templates/packages/ui/src/components/separator.tsx.hbs +26 -0
- package/templates/marketing/payload/src/Footer/config.ts.hbs +0 -178
- package/templates/marketing/payload/src/Footer/index.ts.hbs +0 -1
- package/templates/marketing/payload/src/Header/RowLabel.tsx.hbs +0 -21
- package/templates/marketing/payload/src/Header/config.ts.hbs +0 -208
- package/templates/marketing/payload/src/Header/index.ts.hbs +0 -1
- package/templates/marketing/payload/src/access/index.ts.hbs +0 -3
- package/templates/marketing/payload/src/app/(frontend)/layout.tsx.hbs +0 -19
- package/templates/marketing/payload/src/app/(frontend)/next/seed/route.ts.hbs +0 -31
- package/templates/marketing/payload/src/app/(frontend)/page.tsx.hbs +0 -83
- package/templates/marketing/payload/src/app/(payload)/admin/[[...segments]]/not-found.tsx.hbs +0 -24
- package/templates/marketing/payload/src/app/(payload)/admin/[[...segments]]/page.tsx.hbs +0 -24
- package/templates/marketing/payload/src/app/(payload)/admin/importMap.js.hbs +0 -1
- package/templates/marketing/payload/src/app/(payload)/custom.scss.hbs +0 -1
- package/templates/marketing/payload/src/app/(payload)/layout.tsx.hbs +0 -31
- package/templates/marketing/payload/src/app/globals.css.hbs +0 -83
- package/templates/marketing/payload/src/app/layout.tsx.hbs +0 -10
- package/templates/marketing/payload/src/blocks/Benefits.ts.hbs +0 -34
- package/templates/marketing/payload/src/blocks/CTA.ts.hbs +0 -39
- package/templates/marketing/payload/src/blocks/Content.ts.hbs +0 -9
- package/templates/marketing/payload/src/blocks/FAQ.ts.hbs +0 -18
- package/templates/marketing/payload/src/blocks/Features.ts.hbs +0 -34
- package/templates/marketing/payload/src/blocks/Hero.ts.hbs +0 -40
- package/templates/marketing/payload/src/blocks/LogoBanner.ts.hbs +0 -17
- package/templates/marketing/payload/src/blocks/Pricing.ts.hbs +0 -37
- package/templates/marketing/payload/src/blocks/Testimonials.ts.hbs +0 -21
- package/templates/marketing/payload/src/blocks/index.ts.hbs +0 -9
- package/templates/marketing/payload/src/collections/Categories/index.ts.hbs +0 -28
- package/templates/marketing/payload/src/collections/FAQs/index.ts.hbs +0 -100
- package/templates/marketing/payload/src/collections/Media.ts.hbs +0 -164
- package/templates/marketing/payload/src/collections/Pages/hooks/revalidatePage.ts.hbs +0 -43
- package/templates/marketing/payload/src/collections/Pages/index.ts.hbs +0 -142
- package/templates/marketing/payload/src/collections/Posts/hooks/populateAuthors.ts.hbs +0 -41
- package/templates/marketing/payload/src/collections/Posts/hooks/revalidatePost.ts.hbs +0 -44
- package/templates/marketing/payload/src/collections/Posts/index.ts.hbs +0 -244
- package/templates/marketing/payload/src/collections/Users/index.ts.hbs +0 -26
- package/templates/marketing/payload/src/collections/index.ts.hbs +0 -6
- package/templates/marketing/payload/src/components/BeforeDashboard/SeedButton/index.tsx.hbs +0 -89
- package/templates/marketing/payload/src/components/BeforeDashboard/index.tsx.hbs +0 -69
- package/templates/marketing/payload/src/components/BeforeLogin/index.tsx.hbs +0 -14
- package/templates/marketing/payload/src/components/Link/index.tsx.hbs +0 -79
- package/templates/marketing/payload/src/components/Media/index.tsx.hbs +0 -67
- package/templates/marketing/payload/src/components/RichText/index.tsx.hbs +0 -44
- package/templates/marketing/payload/src/endpoints/seed/home.ts.hbs +0 -76
- package/templates/marketing/payload/src/endpoints/seed/image-1.ts.hbs +0 -5
- package/templates/marketing/payload/src/endpoints/seed/image-2.ts.hbs +0 -5
- package/templates/marketing/payload/src/endpoints/seed/image-hero.ts.hbs +0 -5
- package/templates/marketing/payload/src/endpoints/seed/index.ts.hbs +0 -235
- package/templates/marketing/payload/src/endpoints/seed/post-1.ts.hbs +0 -252
- package/templates/marketing/payload/src/fields/defaultLexical.ts.hbs +0 -73
- package/templates/marketing/payload/src/fields/link.ts.hbs +0 -139
- package/templates/marketing/payload/src/fields/linkGroup.ts.hbs +0 -28
- package/templates/marketing/payload/src/globals/index.ts.hbs +0 -2
- package/templates/marketing/payload/src/heros/HighImpact/index.tsx.hbs +0 -53
- package/templates/marketing/payload/src/heros/LowImpact/index.tsx.hbs +0 -48
- package/templates/marketing/payload/src/heros/MediumImpact/index.tsx.hbs +0 -46
- package/templates/marketing/payload/src/heros/PostHero/index.tsx.hbs +0 -68
- package/templates/marketing/payload/src/heros/ProductShowcase/index.tsx.hbs +0 -88
- package/templates/marketing/payload/src/heros/config.ts.hbs +0 -112
- package/templates/marketing/payload/src/heros/index.ts.hbs +0 -7
- package/templates/marketing/payload/src/hooks/index.ts.hbs +0 -2
- package/templates/marketing/payload/src/hooks/populatePublishedAt.ts.hbs +0 -15
- package/templates/marketing/payload/src/providers/HeaderTheme/index.tsx.hbs +0 -34
- package/templates/marketing/payload/src/providers/Theme/index.tsx.hbs +0 -60
- package/templates/marketing/payload/src/providers/Theme/shared.ts.hbs +0 -17
- package/templates/marketing/payload/src/providers/index.tsx.hbs +0 -18
- package/templates/marketing/payload/src/utilities/deepMerge.ts.hbs +0 -35
- package/templates/marketing/payload/src/utilities/formatAuthors.ts.hbs +0 -24
- package/templates/marketing/payload/src/utilities/formatDateTime.ts.hbs +0 -13
- package/templates/marketing/payload/src/utilities/generateMeta.ts.hbs +0 -87
- package/templates/marketing/payload/src/utilities/generatePreviewPath.ts.hbs +0 -33
- package/templates/marketing/payload/src/utilities/getURL.ts.hbs +0 -26
- package/templates/marketing/payload/src/utilities/index.ts.hbs +0 -8
- package/templates/marketing/payload/src/utilities/mergeOpenGraph.ts.hbs +0 -26
- /package/templates/marketing/payload/src/access/{anyone.ts.hbs → anyone.ts} +0 -0
- /package/templates/marketing/payload/src/components/BeforeDashboard/SeedButton/{index.scss.hbs → index.scss} +0 -0
- /package/templates/marketing/payload/src/components/BeforeDashboard/{index.scss.hbs → index.scss} +0 -0
- /package/templates/marketing/payload/src/fields/{index.ts.hbs → index.ts} +0 -0
- /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
|
+
}
|