create-nextdevtpl 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/catalog.json +385 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +112 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +115 -0
- package/dist/cli.js.map +1 -0
- package/dist/filesystem.d.ts +10 -0
- package/dist/filesystem.js +86 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/generate.d.ts +5 -0
- package/dist/generate.js +758 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +111 -0
- package/dist/selection.js.map +1 -0
- package/dist/service-templates.d.ts +4 -0
- package/dist/service-templates.js +180 -0
- package/dist/service-templates.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
- package/template/.dockerignore +14 -0
- package/template/.env.example +282 -0
- package/template/.gitattributes +17 -0
- package/template/.node-version +1 -0
- package/template/Dockerfile +45 -0
- package/template/LICENSE +21 -0
- package/template/README.md +267 -0
- package/template/biome.json +68 -0
- package/template/cloudflare/templates/logger.ts +162 -0
- package/template/cloudflare/templates/mail/server.ts +6 -0
- package/template/cloudflare/templates/mail/templates.ts +87 -0
- package/template/cloudflare/templates/mail/utils.ts +69 -0
- package/template/cloudflare/worker.mjs +25 -0
- package/template/components.json +22 -0
- package/template/compose.yaml +74 -0
- package/template/deploy/check-health.mjs +25 -0
- package/template/deploy/docker/README.md +14 -0
- package/template/deploy/server/README.md +24 -0
- package/template/deploy/server/build.sh +24 -0
- package/template/deploy/server/nextdevtpl.service +19 -0
- package/template/deploy/server/start.sh +19 -0
- package/template/deploy/vercel/README.md +16 -0
- package/template/deploy/verify-production-health.mjs +86 -0
- package/template/drizzle.config.ts +47 -0
- package/template/gitignore +62 -0
- package/template/manifest.json +35 -0
- package/template/messages/en.json +766 -0
- package/template/messages/zh.json +762 -0
- package/template/next.config.mjs +28 -0
- package/template/open-next.config.ts +3 -0
- package/template/package.json +125 -0
- package/template/pnpm-lock.yaml +15901 -0
- package/template/postcss.config.mjs +7 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/logo.svg +6 -0
- package/template/public/next.svg +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/sentry.client.config.ts +10 -0
- package/template/sentry.edge.config.ts +10 -0
- package/template/sentry.server.config.ts +10 -0
- package/template/source.config.ts +59 -0
- package/template/src/adapters/ai/anthropic.ts +113 -0
- package/template/src/adapters/ai/index.ts +6 -0
- package/template/src/adapters/ai/openai-compatible.ts +88 -0
- package/template/src/adapters/ai/workers-ai.ts +63 -0
- package/template/src/adapters/index.ts +10 -0
- package/template/src/adapters/jobs/cloudflare-workflows.ts +46 -0
- package/template/src/adapters/jobs/index.ts +5 -0
- package/template/src/adapters/jobs/inngest/adapter.ts +29 -0
- package/template/src/adapters/jobs/inngest/client.ts +8 -0
- package/template/src/adapters/jobs/inngest/functions.ts +17 -0
- package/template/src/adapters/jobs/inngest/handler.ts +6 -0
- package/template/src/adapters/jobs/inngest/index.ts +4 -0
- package/template/src/adapters/mail/cloudflare-email.ts +50 -0
- package/template/src/adapters/mail/disabled.ts +17 -0
- package/template/src/adapters/mail/index.ts +7 -0
- package/template/src/adapters/mail/resend.ts +76 -0
- package/template/src/adapters/mail/smtp.ts +82 -0
- package/template/src/adapters/payment/creem.ts +269 -0
- package/template/src/adapters/payment/index.ts +2 -0
- package/template/src/adapters/payment/stripe.ts +302 -0
- package/template/src/adapters/rate-limit/cloudflare.ts +42 -0
- package/template/src/adapters/rate-limit/index.ts +6 -0
- package/template/src/adapters/rate-limit/noop.ts +27 -0
- package/template/src/adapters/rate-limit/upstash.ts +113 -0
- package/template/src/adapters/registry.ts +228 -0
- package/template/src/adapters/storage/index.ts +3 -0
- package/template/src/adapters/storage/r2-binding.ts +105 -0
- package/template/src/adapters/storage/s3-compatible.ts +149 -0
- package/template/src/app/[locale]/(admin)/admin/layout.tsx +36 -0
- package/template/src/app/[locale]/(admin)/admin/page.tsx +345 -0
- package/template/src/app/[locale]/(admin)/admin/tickets/[id]/page.tsx +261 -0
- package/template/src/app/[locale]/(admin)/admin/tickets/page.tsx +236 -0
- package/template/src/app/[locale]/(admin)/admin/users/page.tsx +557 -0
- package/template/src/app/[locale]/(auth)/forgot-password/page.tsx +9 -0
- package/template/src/app/[locale]/(auth)/layout.tsx +28 -0
- package/template/src/app/[locale]/(auth)/sign-in/page.tsx +9 -0
- package/template/src/app/[locale]/(auth)/sign-up/page.tsx +9 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/credits/buy/buy-credits-view.tsx +170 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/credits/buy/page.tsx +12 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/credits/page.tsx +5 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/loading.tsx +41 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/page.tsx +299 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/settings/page.tsx +38 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/support/[id]/page.tsx +220 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/support/new/page.tsx +183 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/support/page.tsx +145 -0
- package/template/src/app/[locale]/(dashboard)/layout.tsx +35 -0
- package/template/src/app/[locale]/(marketing)/blog/[slug]/page.tsx +178 -0
- package/template/src/app/[locale]/(marketing)/blog/blog-post-card.tsx +95 -0
- package/template/src/app/[locale]/(marketing)/blog/page.tsx +127 -0
- package/template/src/app/[locale]/(marketing)/demo/plan-badges/page.tsx +36 -0
- package/template/src/app/[locale]/(marketing)/layout.tsx +15 -0
- package/template/src/app/[locale]/(marketing)/legal/[slug]/page.tsx +130 -0
- package/template/src/app/[locale]/(marketing)/page.tsx +94 -0
- package/template/src/app/[locale]/(marketing)/pseo/[slug]/page.tsx +68 -0
- package/template/src/app/[locale]/(marketing)/pseo/page.tsx +137 -0
- package/template/src/app/[locale]/docs/[[...slug]]/page.tsx +63 -0
- package/template/src/app/[locale]/docs/layout.tsx +39 -0
- package/template/src/app/[locale]/layout.tsx +80 -0
- package/template/src/app/[locale]/opengraph-image.tsx +20 -0
- package/template/src/app/[locale]/twitter-image.tsx +20 -0
- package/template/src/app/api/auth/[...all]/route.ts +20 -0
- package/template/src/app/api/health/route.ts +45 -0
- package/template/src/app/api/inngest/route.ts +12 -0
- package/template/src/app/api/jobs/credits/expire/route.ts +102 -0
- package/template/src/app/api/search/route.ts +17 -0
- package/template/src/app/api/upload/presigned/route.ts +98 -0
- package/template/src/app/api/webhooks/payment/route.ts +363 -0
- package/template/src/app/globals.css +406 -0
- package/template/src/app/icon.svg +6 -0
- package/template/src/app/image-proxy/[...path]/route.ts +137 -0
- package/template/src/app/layout.tsx +73 -0
- package/template/src/app/robots.ts +29 -0
- package/template/src/app/sitemap.ts +68 -0
- package/template/src/components/motion/reveal.tsx +49 -0
- package/template/src/components/seo/json-ld.tsx +87 -0
- package/template/src/components/seo/og-image-template.tsx +118 -0
- package/template/src/components/ui/accordion.tsx +66 -0
- package/template/src/components/ui/alert-dialog.tsx +195 -0
- package/template/src/components/ui/avatar.tsx +53 -0
- package/template/src/components/ui/badge.tsx +46 -0
- package/template/src/components/ui/button.tsx +65 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/checkbox.tsx +32 -0
- package/template/src/components/ui/collapsible.tsx +33 -0
- package/template/src/components/ui/dialog.tsx +124 -0
- package/template/src/components/ui/dropdown-menu.tsx +257 -0
- package/template/src/components/ui/form.tsx +233 -0
- package/template/src/components/ui/index.ts +2 -0
- package/template/src/components/ui/input.tsx +21 -0
- package/template/src/components/ui/label.tsx +24 -0
- package/template/src/components/ui/navigation-menu.tsx +167 -0
- package/template/src/components/ui/pagination.tsx +125 -0
- package/template/src/components/ui/popover.tsx +48 -0
- package/template/src/components/ui/progress.tsx +27 -0
- package/template/src/components/ui/radio-group.tsx +45 -0
- package/template/src/components/ui/select.tsx +190 -0
- package/template/src/components/ui/separator.tsx +28 -0
- package/template/src/components/ui/sheet.tsx +140 -0
- package/template/src/components/ui/switch.tsx +31 -0
- package/template/src/components/ui/tabs.tsx +75 -0
- package/template/src/components/ui/textarea.tsx +18 -0
- package/template/src/config/index.ts +31 -0
- package/template/src/config/nav.ts +238 -0
- package/template/src/config/payment.ts +282 -0
- package/template/src/config/site.ts +53 -0
- package/template/src/config/subscription-plan.ts +164 -0
- package/template/src/content/blog/en/hello-world.mdx +48 -0
- package/template/src/content/blog/zh/hello-world.mdx +48 -0
- package/template/src/content/blog/zh/nextdevtpl-linuxdo-intro.mdx +98 -0
- package/template/src/content/docs/en/admin-and-support.mdx +65 -0
- package/template/src/content/docs/en/authentication.mdx +71 -0
- package/template/src/content/docs/en/background-jobs.mdx +53 -0
- package/template/src/content/docs/en/configuration.mdx +71 -0
- package/template/src/content/docs/en/credits.mdx +69 -0
- package/template/src/content/docs/en/customization.mdx +95 -0
- package/template/src/content/docs/en/database.mdx +66 -0
- package/template/src/content/docs/en/deployment.mdx +62 -0
- package/template/src/content/docs/en/email.mdx +48 -0
- package/template/src/content/docs/en/index.mdx +48 -0
- package/template/src/content/docs/en/internationalization.mdx +78 -0
- package/template/src/content/docs/en/meta.json +22 -0
- package/template/src/content/docs/en/observability.mdx +69 -0
- package/template/src/content/docs/en/payments.mdx +64 -0
- package/template/src/content/docs/en/project-structure.mdx +82 -0
- package/template/src/content/docs/en/quick-start.mdx +113 -0
- package/template/src/content/docs/en/storage.mdx +51 -0
- package/template/src/content/docs/zh/admin-and-support.mdx +65 -0
- package/template/src/content/docs/zh/authentication.mdx +71 -0
- package/template/src/content/docs/zh/background-jobs.mdx +53 -0
- package/template/src/content/docs/zh/configuration.mdx +70 -0
- package/template/src/content/docs/zh/credits.mdx +69 -0
- package/template/src/content/docs/zh/customization.mdx +95 -0
- package/template/src/content/docs/zh/database.mdx +66 -0
- package/template/src/content/docs/zh/deployment.mdx +61 -0
- package/template/src/content/docs/zh/email.mdx +47 -0
- package/template/src/content/docs/zh/index.mdx +48 -0
- package/template/src/content/docs/zh/internationalization.mdx +78 -0
- package/template/src/content/docs/zh/meta.json +22 -0
- package/template/src/content/docs/zh/observability.mdx +68 -0
- package/template/src/content/docs/zh/payments.mdx +64 -0
- package/template/src/content/docs/zh/project-structure.mdx +82 -0
- package/template/src/content/docs/zh/quick-start.mdx +110 -0
- package/template/src/content/docs/zh/storage.mdx +51 -0
- package/template/src/content/legal/en/cookie-policy.mdx +78 -0
- package/template/src/content/legal/en/privacy.mdx +131 -0
- package/template/src/content/legal/en/terms.mdx +93 -0
- package/template/src/content/legal/zh/cookie-policy.mdx +78 -0
- package/template/src/content/legal/zh/privacy.mdx +131 -0
- package/template/src/content/legal/zh/terms.mdx +92 -0
- package/template/src/core/modules/contract.ts +225 -0
- package/template/src/core/modules/index.ts +1 -0
- package/template/src/core/services/ai.ts +31 -0
- package/template/src/core/services/common.ts +112 -0
- package/template/src/core/services/index.ts +8 -0
- package/template/src/core/services/jobs.ts +24 -0
- package/template/src/core/services/mail.ts +35 -0
- package/template/src/core/services/manifest.ts +51 -0
- package/template/src/core/services/payment.ts +92 -0
- package/template/src/core/services/rate-limit.ts +47 -0
- package/template/src/core/services/storage.ts +42 -0
- package/template/src/db/index.ts +71 -0
- package/template/src/db/schema/auth.ts +67 -0
- package/template/src/db/schema/credits.ts +94 -0
- package/template/src/db/schema/index.ts +5 -0
- package/template/src/db/schema/mail.ts +14 -0
- package/template/src/db/schema/subscription.ts +21 -0
- package/template/src/db/schema/support.ts +56 -0
- package/template/src/db/schema.ts +2 -0
- package/template/src/features/admin/components/admin-sidebar.tsx +213 -0
- package/template/src/features/admin/components/index.ts +2 -0
- package/template/src/features/admin/index.ts +3 -0
- package/template/src/features/admin/manifest.ts +41 -0
- package/template/src/features/analytics/components/analytics.tsx +62 -0
- package/template/src/features/analytics/components/index.ts +2 -0
- package/template/src/features/analytics/index.ts +3 -0
- package/template/src/features/analytics/manifest.ts +13 -0
- package/template/src/features/auth/components/auth-brand-panel.tsx +82 -0
- package/template/src/features/auth/components/auth-error-alert.tsx +28 -0
- package/template/src/features/auth/components/auth-footer.tsx +52 -0
- package/template/src/features/auth/components/auth-logo.tsx +30 -0
- package/template/src/features/auth/components/forgot-password-form.tsx +146 -0
- package/template/src/features/auth/components/index.ts +9 -0
- package/template/src/features/auth/components/sign-in-form.tsx +241 -0
- package/template/src/features/auth/components/sign-up-form.tsx +307 -0
- package/template/src/features/auth/index.ts +3 -0
- package/template/src/features/auth/manifest.ts +30 -0
- package/template/src/features/blog/components/blog-post-item.tsx +72 -0
- package/template/src/features/blog/components/index.ts +1 -0
- package/template/src/features/blog/data/mock-posts.ts +62 -0
- package/template/src/features/blog/index.ts +3 -0
- package/template/src/features/blog/manifest.ts +22 -0
- package/template/src/features/credits/account.ts +76 -0
- package/template/src/features/credits/actions.ts +427 -0
- package/template/src/features/credits/components/credit-balance-badge.tsx +54 -0
- package/template/src/features/credits/components/credit-usage-section.tsx +160 -0
- package/template/src/features/credits/components/index.ts +7 -0
- package/template/src/features/credits/components/transaction-history.tsx +346 -0
- package/template/src/features/credits/config.ts +70 -0
- package/template/src/features/credits/consume.ts +140 -0
- package/template/src/features/credits/core.ts +23 -0
- package/template/src/features/credits/errors.ts +26 -0
- package/template/src/features/credits/expire.ts +112 -0
- package/template/src/features/credits/grant.ts +168 -0
- package/template/src/features/credits/index.ts +6 -0
- package/template/src/features/credits/manifest.ts +36 -0
- package/template/src/features/credits/query.ts +65 -0
- package/template/src/features/credits/types.ts +48 -0
- package/template/src/features/dashboard/components/index.ts +3 -0
- package/template/src/features/dashboard/components/main-wrapper.tsx +100 -0
- package/template/src/features/dashboard/components/sidebar.tsx +341 -0
- package/template/src/features/dashboard/context/index.ts +2 -0
- package/template/src/features/dashboard/context/sidebar-context.tsx +72 -0
- package/template/src/features/dashboard/index.ts +4 -0
- package/template/src/features/dashboard/manifest.ts +28 -0
- package/template/src/features/mail/actions.ts +175 -0
- package/template/src/features/mail/index.ts +5 -0
- package/template/src/features/mail/manifest.ts +13 -0
- package/template/src/features/mail/server.ts +8 -0
- package/template/src/features/mail/templates/index.ts +11 -0
- package/template/src/features/mail/templates/primary-action-email.tsx +224 -0
- package/template/src/features/mail/templates/welcome-email.tsx +121 -0
- package/template/src/features/mail/utils.ts +190 -0
- package/template/src/features/marketing/components/animated-price.tsx +51 -0
- package/template/src/features/marketing/components/cookie-consent.tsx +255 -0
- package/template/src/features/marketing/components/cta-section.tsx +50 -0
- package/template/src/features/marketing/components/faq-section.tsx +59 -0
- package/template/src/features/marketing/components/feature-grid.tsx +80 -0
- package/template/src/features/marketing/components/footer.tsx +128 -0
- package/template/src/features/marketing/components/header.tsx +255 -0
- package/template/src/features/marketing/components/hero-section.tsx +323 -0
- package/template/src/features/marketing/components/how-it-works.tsx +115 -0
- package/template/src/features/marketing/components/index.ts +14 -0
- package/template/src/features/marketing/components/nav-menu.tsx +236 -0
- package/template/src/features/marketing/components/pricing-section.tsx +446 -0
- package/template/src/features/marketing/components/testimonials.tsx +59 -0
- package/template/src/features/marketing/components/use-cases-section.tsx +78 -0
- package/template/src/features/marketing/index.ts +3 -0
- package/template/src/features/marketing/manifest.ts +35 -0
- package/template/src/features/payment/actions.ts +182 -0
- package/template/src/features/payment/index.ts +4 -0
- package/template/src/features/payment/manifest.ts +18 -0
- package/template/src/features/payment/server.ts +7 -0
- package/template/src/features/payment/types.ts +202 -0
- package/template/src/features/pseo/components/index.ts +6 -0
- package/template/src/features/pseo/components/pseo-cta.tsx +51 -0
- package/template/src/features/pseo/components/pseo-faq.tsx +40 -0
- package/template/src/features/pseo/components/pseo-feature-grid.tsx +74 -0
- package/template/src/features/pseo/components/pseo-hero.tsx +106 -0
- package/template/src/features/pseo/components/pseo-related.tsx +62 -0
- package/template/src/features/pseo/components/pseo-use-cases.tsx +50 -0
- package/template/src/features/pseo/data/pseo-pages.json +534 -0
- package/template/src/features/pseo/index.ts +4 -0
- package/template/src/features/pseo/lib/pseo-data.ts +140 -0
- package/template/src/features/pseo/manifest.ts +22 -0
- package/template/src/features/settings/actions/delete-account.ts +50 -0
- package/template/src/features/settings/actions/index.ts +3 -0
- package/template/src/features/settings/actions/update-profile.ts +49 -0
- package/template/src/features/settings/components/billing-section.tsx +283 -0
- package/template/src/features/settings/components/index.ts +3 -0
- package/template/src/features/settings/components/profile-form.tsx +154 -0
- package/template/src/features/settings/components/security-section.tsx +158 -0
- package/template/src/features/settings/components/settings-profile-view.tsx +506 -0
- package/template/src/features/settings/index.ts +5 -0
- package/template/src/features/settings/manifest.ts +28 -0
- package/template/src/features/settings/schemas/index.ts +2 -0
- package/template/src/features/settings/schemas/update-profile.ts +21 -0
- package/template/src/features/shared/components/cookie-settings-dialog.tsx +282 -0
- package/template/src/features/shared/components/index.ts +5 -0
- package/template/src/features/shared/components/language-switcher.tsx +82 -0
- package/template/src/features/shared/components/max-width-wrapper.tsx +49 -0
- package/template/src/features/shared/components/mode-toggle.tsx +119 -0
- package/template/src/features/shared/icons/google-icon.tsx +39 -0
- package/template/src/features/shared/icons/index.ts +7 -0
- package/template/src/features/shared/index.ts +5 -0
- package/template/src/features/shared/manifest.ts +13 -0
- package/template/src/features/shared/providers.tsx +59 -0
- package/template/src/features/storage/actions.ts +107 -0
- package/template/src/features/storage/index.ts +6 -0
- package/template/src/features/storage/manifest.ts +22 -0
- package/template/src/features/storage/types.ts +143 -0
- package/template/src/features/storage/utils.ts +85 -0
- package/template/src/features/storage/validation.ts +151 -0
- package/template/src/features/subscription/actions/get-user-plan.ts +26 -0
- package/template/src/features/subscription/actions/index.ts +5 -0
- package/template/src/features/subscription/components/current-plan-badge.tsx +18 -0
- package/template/src/features/subscription/components/index.ts +6 -0
- package/template/src/features/subscription/components/plan-badge.tsx +98 -0
- package/template/src/features/subscription/index.ts +25 -0
- package/template/src/features/subscription/manifest.ts +13 -0
- package/template/src/features/subscription/services/user-plan.ts +181 -0
- package/template/src/features/support/actions/admin-ticket.ts +172 -0
- package/template/src/features/support/actions/admin-users.ts +295 -0
- package/template/src/features/support/actions/index.ts +22 -0
- package/template/src/features/support/actions/ticket.ts +12 -0
- package/template/src/features/support/actions/user-ticket.ts +153 -0
- package/template/src/features/support/components/admin-ticket-reply-form.tsx +109 -0
- package/template/src/features/support/components/admin-ticket-status-select.tsx +119 -0
- package/template/src/features/support/components/index.ts +6 -0
- package/template/src/features/support/components/ticket-message-form.tsx +99 -0
- package/template/src/features/support/components/user-role-select.tsx +107 -0
- package/template/src/features/support/index.ts +5 -0
- package/template/src/features/support/manifest.ts +53 -0
- package/template/src/features/support/schemas/index.ts +15 -0
- package/template/src/features/support/schemas/ticket.ts +81 -0
- package/template/src/hooks/index.ts +2 -0
- package/template/src/i18n/index.ts +12 -0
- package/template/src/i18n/request.ts +24 -0
- package/template/src/i18n/routing.ts +23 -0
- package/template/src/instrumentation.ts +19 -0
- package/template/src/lib/ai/index.ts +16 -0
- package/template/src/lib/api-logger.ts +24 -0
- package/template/src/lib/auth/admin.ts +54 -0
- package/template/src/lib/auth/api.ts +17 -0
- package/template/src/lib/auth/client.ts +227 -0
- package/template/src/lib/auth/edge.ts +84 -0
- package/template/src/lib/auth/errors.ts +99 -0
- package/template/src/lib/auth/index.ts +159 -0
- package/template/src/lib/auth/server.ts +46 -0
- package/template/src/lib/auth/session-context.tsx +96 -0
- package/template/src/lib/cloudflare/bindings.ts +31 -0
- package/template/src/lib/cookie-consent.ts +33 -0
- package/template/src/lib/file-utils.ts +45 -0
- package/template/src/lib/fingerprint/index.ts +73 -0
- package/template/src/lib/health/checks.ts +6 -0
- package/template/src/lib/health/core.ts +86 -0
- package/template/src/lib/health/index.ts +11 -0
- package/template/src/lib/health/types.ts +13 -0
- package/template/src/lib/index.ts +2 -0
- package/template/src/lib/logger/index.ts +258 -0
- package/template/src/lib/monitoring/index.ts +384 -0
- package/template/src/lib/rate-limit/anonymous.ts +85 -0
- package/template/src/lib/rate-limit/index.ts +111 -0
- package/template/src/lib/safe-action.ts +127 -0
- package/template/src/lib/seo/json-ld.ts +188 -0
- package/template/src/lib/source.ts +122 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/middleware.ts +162 -0
- package/template/src/modules/index.ts +10 -0
- package/template/src/modules/presets.ts +8 -0
- package/template/src/modules/registry.ts +34 -0
- package/template/src/modules/selection.ts +45 -0
- package/template/src/services/ai.ts +49 -0
- package/template/src/services/index.ts +6 -0
- package/template/src/services/jobs.ts +3 -0
- package/template/src/services/mail.ts +7 -0
- package/template/src/services/payment.ts +3 -0
- package/template/src/services/rate-limit.ts +12 -0
- package/template/src/services/storage.ts +3 -0
- package/template/src/types/fumadocs-source.d.ts +14 -0
- package/template/src/types/index.ts +2 -0
- package/template/tsconfig.json +53 -0
- package/template/vercel.json +11 -0
- package/template/vitest.config.ts +51 -0
- package/template/wrangler.jsonc +30 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Button,
|
|
4
|
+
Container,
|
|
5
|
+
Head,
|
|
6
|
+
Heading,
|
|
7
|
+
Hr,
|
|
8
|
+
Html,
|
|
9
|
+
Preview,
|
|
10
|
+
Section,
|
|
11
|
+
Text,
|
|
12
|
+
} from "@react-email/components";
|
|
13
|
+
import { Tailwind } from "@react-email/tailwind";
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 通用操作邮件模板
|
|
18
|
+
*
|
|
19
|
+
* 用于需要用户执行某个操作的邮件场景:
|
|
20
|
+
* - 重置密码
|
|
21
|
+
* - Magic Link 登录
|
|
22
|
+
* - 邮箱验证
|
|
23
|
+
* - 邀请确认
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface PrimaryActionEmailProps {
|
|
27
|
+
/** 预览文本 (显示在收件箱列表中) */
|
|
28
|
+
preview?: string;
|
|
29
|
+
/** 操作按钮文本 */
|
|
30
|
+
actionLabel: string;
|
|
31
|
+
/** 操作链接 */
|
|
32
|
+
actionUrl: string;
|
|
33
|
+
/** 邮件正文内容 */
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
/** 链接有效期提示 (可选) */
|
|
36
|
+
expiresIn?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 通用操作邮件组件
|
|
41
|
+
*/
|
|
42
|
+
export function PrimaryActionEmail({
|
|
43
|
+
preview,
|
|
44
|
+
actionLabel,
|
|
45
|
+
actionUrl,
|
|
46
|
+
children,
|
|
47
|
+
expiresIn,
|
|
48
|
+
}: PrimaryActionEmailProps) {
|
|
49
|
+
return (
|
|
50
|
+
<Html>
|
|
51
|
+
<Head />
|
|
52
|
+
{preview && <Preview>{preview}</Preview>}
|
|
53
|
+
<Tailwind>
|
|
54
|
+
<Body className="mx-auto my-auto bg-white font-sans">
|
|
55
|
+
<Container className="mx-auto my-10 max-w-xl rounded-lg border border-solid border-gray-200 p-8">
|
|
56
|
+
{/* Logo / 品牌区域 */}
|
|
57
|
+
<Section className="mb-8 text-center">
|
|
58
|
+
<Heading className="m-0 text-2xl font-bold text-gray-900">
|
|
59
|
+
NextDevTpl
|
|
60
|
+
</Heading>
|
|
61
|
+
</Section>
|
|
62
|
+
|
|
63
|
+
{/* 正文内容 (通过 children 传入) */}
|
|
64
|
+
<Section className="mb-6">{children}</Section>
|
|
65
|
+
|
|
66
|
+
{/* CTA 按钮 */}
|
|
67
|
+
<Section className="mb-6 text-center">
|
|
68
|
+
<Button
|
|
69
|
+
href={actionUrl}
|
|
70
|
+
className="inline-block rounded-md bg-violet-600 px-6 py-3 text-center text-sm font-semibold text-white no-underline"
|
|
71
|
+
>
|
|
72
|
+
{actionLabel}
|
|
73
|
+
</Button>
|
|
74
|
+
</Section>
|
|
75
|
+
|
|
76
|
+
{/* 链接有效期提示 */}
|
|
77
|
+
{expiresIn && (
|
|
78
|
+
<Text className="mb-4 text-center text-sm text-gray-500">
|
|
79
|
+
This link will expire in {expiresIn}.
|
|
80
|
+
</Text>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{/* 备用链接 */}
|
|
84
|
+
<Section className="mb-6 rounded-lg bg-gray-50 p-4">
|
|
85
|
+
<Text className="m-0 mb-2 text-xs text-gray-600">
|
|
86
|
+
If the button above doesn't work, copy and paste this URL
|
|
87
|
+
into your browser:
|
|
88
|
+
</Text>
|
|
89
|
+
<Text className="m-0 break-all text-xs text-violet-600">
|
|
90
|
+
{actionUrl}
|
|
91
|
+
</Text>
|
|
92
|
+
</Section>
|
|
93
|
+
|
|
94
|
+
<Hr className="my-6 border-gray-200" />
|
|
95
|
+
|
|
96
|
+
{/* 安全提示 */}
|
|
97
|
+
<Text className="m-0 text-center text-xs text-gray-500">
|
|
98
|
+
If you didn't request this email, you can safely ignore it.
|
|
99
|
+
</Text>
|
|
100
|
+
|
|
101
|
+
{/* 页脚 */}
|
|
102
|
+
<Text className="m-0 mt-4 text-center text-xs text-gray-400">
|
|
103
|
+
© {new Date().getFullYear()} NextDevTpl. All rights reserved.
|
|
104
|
+
</Text>
|
|
105
|
+
</Container>
|
|
106
|
+
</Body>
|
|
107
|
+
</Tailwind>
|
|
108
|
+
</Html>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 默认导出 - 用于 React Email 预览
|
|
114
|
+
*/
|
|
115
|
+
export default PrimaryActionEmail;
|
|
116
|
+
|
|
117
|
+
// ============================================
|
|
118
|
+
// 预定义的邮件变体
|
|
119
|
+
// ============================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Magic Link 登录邮件
|
|
123
|
+
*/
|
|
124
|
+
export function MagicLinkEmail({
|
|
125
|
+
magicLinkUrl,
|
|
126
|
+
email,
|
|
127
|
+
}: {
|
|
128
|
+
magicLinkUrl: string;
|
|
129
|
+
email: string;
|
|
130
|
+
}) {
|
|
131
|
+
return (
|
|
132
|
+
<PrimaryActionEmail
|
|
133
|
+
preview="Sign in to NextDevTpl with this magic link"
|
|
134
|
+
actionLabel="Sign In"
|
|
135
|
+
actionUrl={magicLinkUrl}
|
|
136
|
+
expiresIn="15 minutes"
|
|
137
|
+
>
|
|
138
|
+
<Heading className="mb-4 text-xl font-semibold text-gray-900">
|
|
139
|
+
Sign in to NextDevTpl
|
|
140
|
+
</Heading>
|
|
141
|
+
<Text className="mb-4 text-base leading-relaxed text-gray-600">
|
|
142
|
+
Click the button below to sign in to your account ({email}). No password
|
|
143
|
+
required!
|
|
144
|
+
</Text>
|
|
145
|
+
</PrimaryActionEmail>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 重置密码邮件
|
|
151
|
+
*/
|
|
152
|
+
export function ResetPasswordEmail({
|
|
153
|
+
resetUrl,
|
|
154
|
+
name,
|
|
155
|
+
}: {
|
|
156
|
+
resetUrl: string;
|
|
157
|
+
name: string;
|
|
158
|
+
}) {
|
|
159
|
+
return (
|
|
160
|
+
<PrimaryActionEmail
|
|
161
|
+
preview="Reset your NextDevTpl password"
|
|
162
|
+
actionLabel="Reset Password"
|
|
163
|
+
actionUrl={resetUrl}
|
|
164
|
+
expiresIn="1 hour"
|
|
165
|
+
>
|
|
166
|
+
<Heading className="mb-4 text-xl font-semibold text-gray-900">
|
|
167
|
+
Reset Your Password
|
|
168
|
+
</Heading>
|
|
169
|
+
<Text className="mb-4 text-base leading-relaxed text-gray-600">
|
|
170
|
+
Hi {name}, we received a request to reset your password. Click the
|
|
171
|
+
button below to create a new password.
|
|
172
|
+
</Text>
|
|
173
|
+
</PrimaryActionEmail>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 邮箱验证邮件
|
|
179
|
+
*/
|
|
180
|
+
export function VerifyEmailEmail({
|
|
181
|
+
verifyUrl,
|
|
182
|
+
name,
|
|
183
|
+
}: {
|
|
184
|
+
verifyUrl: string;
|
|
185
|
+
name: string;
|
|
186
|
+
}) {
|
|
187
|
+
return (
|
|
188
|
+
<PrimaryActionEmail
|
|
189
|
+
preview="Verify your email address"
|
|
190
|
+
actionLabel="Verify Email"
|
|
191
|
+
actionUrl={verifyUrl}
|
|
192
|
+
expiresIn="24 hours"
|
|
193
|
+
>
|
|
194
|
+
<Heading className="mb-4 text-xl font-semibold text-gray-900">
|
|
195
|
+
Verify Your Email
|
|
196
|
+
</Heading>
|
|
197
|
+
<Text className="mb-4 text-base leading-relaxed text-gray-600">
|
|
198
|
+
Hi {name}, please verify your email address by clicking the button
|
|
199
|
+
below. This helps us keep your account secure.
|
|
200
|
+
</Text>
|
|
201
|
+
</PrimaryActionEmail>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 预览 Props
|
|
207
|
+
*/
|
|
208
|
+
PrimaryActionEmail.PreviewProps = {
|
|
209
|
+
preview: "Complete this action",
|
|
210
|
+
actionLabel: "Click Here",
|
|
211
|
+
actionUrl: "https://example.com/action",
|
|
212
|
+
children: (
|
|
213
|
+
<>
|
|
214
|
+
<Heading className="mb-4 text-xl font-semibold text-gray-900">
|
|
215
|
+
Action Required
|
|
216
|
+
</Heading>
|
|
217
|
+
<Text className="mb-4 text-base leading-relaxed text-gray-600">
|
|
218
|
+
This is a sample email that requires you to take an action. Click the
|
|
219
|
+
button below to continue.
|
|
220
|
+
</Text>
|
|
221
|
+
</>
|
|
222
|
+
),
|
|
223
|
+
expiresIn: "24 hours",
|
|
224
|
+
} satisfies PrimaryActionEmailProps;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Button,
|
|
4
|
+
Container,
|
|
5
|
+
Head,
|
|
6
|
+
Heading,
|
|
7
|
+
Hr,
|
|
8
|
+
Html,
|
|
9
|
+
Link,
|
|
10
|
+
Preview,
|
|
11
|
+
Section,
|
|
12
|
+
Text,
|
|
13
|
+
} from "@react-email/components";
|
|
14
|
+
import { Tailwind } from "@react-email/tailwind";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 欢迎邮件模板
|
|
18
|
+
*
|
|
19
|
+
* 新用户注册成功后发送
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface WelcomeEmailProps {
|
|
23
|
+
/** 用户名称 */
|
|
24
|
+
name: string;
|
|
25
|
+
/** 仪表盘链接 */
|
|
26
|
+
dashboardUrl: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 欢迎邮件组件
|
|
31
|
+
*/
|
|
32
|
+
export function WelcomeEmail({ name, dashboardUrl }: WelcomeEmailProps) {
|
|
33
|
+
return (
|
|
34
|
+
<Html>
|
|
35
|
+
<Head />
|
|
36
|
+
<Preview>Welcome to NextDevTpl - Welcome aboard!</Preview>
|
|
37
|
+
<Tailwind>
|
|
38
|
+
<Body className="mx-auto my-auto bg-white font-sans">
|
|
39
|
+
<Container className="mx-auto my-10 max-w-xl rounded-lg border border-solid border-gray-200 p-8">
|
|
40
|
+
{/* Logo / 品牌区域 */}
|
|
41
|
+
<Section className="mb-8 text-center">
|
|
42
|
+
<Heading className="m-0 text-2xl font-bold text-gray-900">
|
|
43
|
+
NextDevTpl
|
|
44
|
+
</Heading>
|
|
45
|
+
</Section>
|
|
46
|
+
|
|
47
|
+
{/* 主标题 */}
|
|
48
|
+
<Heading className="mb-4 text-xl font-semibold text-gray-900">
|
|
49
|
+
Welcome, {name}! 🎉
|
|
50
|
+
</Heading>
|
|
51
|
+
|
|
52
|
+
{/* 正文内容 */}
|
|
53
|
+
<Text className="mb-4 text-base leading-relaxed text-gray-600">
|
|
54
|
+
We're thrilled to have you join NextDevTpl! Your account has
|
|
55
|
+
been successfully created and you're ready to start creating
|
|
56
|
+
flashcards.
|
|
57
|
+
</Text>
|
|
58
|
+
|
|
59
|
+
<Text className="mb-6 text-base leading-relaxed text-gray-600">
|
|
60
|
+
As a welcome gift, we've added{" "}
|
|
61
|
+
<strong>100 free credits</strong> to your account. Use them to
|
|
62
|
+
generate AI-powered flashcards from your learning materials.
|
|
63
|
+
</Text>
|
|
64
|
+
|
|
65
|
+
{/* CTA 按钮 */}
|
|
66
|
+
<Section className="mb-8 text-center">
|
|
67
|
+
<Button
|
|
68
|
+
href={dashboardUrl}
|
|
69
|
+
className="inline-block rounded-md bg-violet-600 px-6 py-3 text-center text-sm font-semibold text-white no-underline"
|
|
70
|
+
>
|
|
71
|
+
Go to Dashboard
|
|
72
|
+
</Button>
|
|
73
|
+
</Section>
|
|
74
|
+
|
|
75
|
+
{/* 快速入门提示 */}
|
|
76
|
+
<Section className="mb-6 rounded-lg bg-gray-50 p-4">
|
|
77
|
+
<Text className="m-0 mb-2 text-sm font-semibold text-gray-900">
|
|
78
|
+
Quick Start Tips:
|
|
79
|
+
</Text>
|
|
80
|
+
<Text className="m-0 text-sm text-gray-600">
|
|
81
|
+
• Upload PDF, Word, or Markdown files to generate flashcards
|
|
82
|
+
<br />• Paste text or URLs to create cards instantly
|
|
83
|
+
<br />• Export to Anki format for spaced repetition learning
|
|
84
|
+
</Text>
|
|
85
|
+
</Section>
|
|
86
|
+
|
|
87
|
+
<Hr className="my-6 border-gray-200" />
|
|
88
|
+
|
|
89
|
+
{/* 页脚 */}
|
|
90
|
+
<Text className="m-0 text-center text-xs text-gray-500">
|
|
91
|
+
Need help? Reply to this email or visit our{" "}
|
|
92
|
+
<Link
|
|
93
|
+
href="https://example.com/docs"
|
|
94
|
+
className="text-violet-600 no-underline"
|
|
95
|
+
>
|
|
96
|
+
documentation
|
|
97
|
+
</Link>
|
|
98
|
+
.
|
|
99
|
+
</Text>
|
|
100
|
+
<Text className="m-0 mt-2 text-center text-xs text-gray-400">
|
|
101
|
+
© {new Date().getFullYear()} NextDevTpl. All rights reserved.
|
|
102
|
+
</Text>
|
|
103
|
+
</Container>
|
|
104
|
+
</Body>
|
|
105
|
+
</Tailwind>
|
|
106
|
+
</Html>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 默认导出 - 用于 React Email 预览
|
|
112
|
+
*/
|
|
113
|
+
export default WelcomeEmail;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 预览时的默认 Props
|
|
117
|
+
*/
|
|
118
|
+
WelcomeEmail.PreviewProps = {
|
|
119
|
+
name: "John Doe",
|
|
120
|
+
dashboardUrl: "https://example.com/dashboard",
|
|
121
|
+
} satisfies WelcomeEmailProps;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { render, toPlainText } from "@react-email/render";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
|
|
4
|
+
import type { MailAddress } from "@/core/services";
|
|
5
|
+
import { mailService } from "@/services/mail";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_FROM_EMAIL =
|
|
8
|
+
process.env.EMAIL_FROM ?? "NextDevTpl <noreply@example.com>";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 邮件发送工具
|
|
12
|
+
*
|
|
13
|
+
* 提供统一的邮件发送接口,支持开发环境模拟和生产环境发送
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// 类型定义
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 发送邮件参数
|
|
22
|
+
*/
|
|
23
|
+
export interface SendEmailParams {
|
|
24
|
+
/** 收件人邮箱 (单个或数组) */
|
|
25
|
+
to: string | string[];
|
|
26
|
+
/** 邮件主题 */
|
|
27
|
+
subject: string;
|
|
28
|
+
/** React Email 组件 */
|
|
29
|
+
react: ReactElement;
|
|
30
|
+
/** 发件人地址 (可选,默认使用 DEFAULT_FROM_EMAIL) */
|
|
31
|
+
from?: string;
|
|
32
|
+
/** 抄送地址 */
|
|
33
|
+
cc?: string | string[];
|
|
34
|
+
/** 密送地址 */
|
|
35
|
+
bcc?: string | string[];
|
|
36
|
+
/** 回复地址 */
|
|
37
|
+
replyTo?: string | string[];
|
|
38
|
+
/** 强制发送 (开发环境下也发送真实邮件) */
|
|
39
|
+
force?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 邮件发送结果
|
|
44
|
+
*/
|
|
45
|
+
export interface SendEmailResult {
|
|
46
|
+
/** 是否成功 */
|
|
47
|
+
success: boolean;
|
|
48
|
+
/** 邮件供应商返回的邮件 ID (生产环境) */
|
|
49
|
+
id?: string;
|
|
50
|
+
/** 错误信息 */
|
|
51
|
+
error?: string;
|
|
52
|
+
/** 是否为模拟发送 (开发环境) */
|
|
53
|
+
simulated?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseMailAddress(value: string): MailAddress {
|
|
57
|
+
const match = /^\s*(.*?)\s*<([^>]+)>\s*$/.exec(value);
|
|
58
|
+
if (!match) {
|
|
59
|
+
return { email: value.trim() };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const email = match[2];
|
|
63
|
+
if (!email) {
|
|
64
|
+
return { email: value.trim() };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const name = match[1]?.trim();
|
|
68
|
+
return name ? { email, name } : { email };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================
|
|
72
|
+
// 工具函数
|
|
73
|
+
// ============================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 判断是否为开发环境
|
|
77
|
+
*/
|
|
78
|
+
function isDevelopment(): boolean {
|
|
79
|
+
return process.env.NODE_ENV === "development";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 在控制台输出邮件预览信息 (开发环境)
|
|
84
|
+
*/
|
|
85
|
+
function logEmailPreview(params: SendEmailParams): void {
|
|
86
|
+
const recipients = Array.isArray(params.to)
|
|
87
|
+
? params.to.join(", ")
|
|
88
|
+
: params.to;
|
|
89
|
+
|
|
90
|
+
console.log(`\n${"=".repeat(60)}`);
|
|
91
|
+
console.log("📧 EMAIL PREVIEW (Development Mode)");
|
|
92
|
+
console.log("=".repeat(60));
|
|
93
|
+
console.log(`To: ${recipients}`);
|
|
94
|
+
console.log(`From: ${params.from ?? DEFAULT_FROM_EMAIL}`);
|
|
95
|
+
console.log(`Subject: ${params.subject}`);
|
|
96
|
+
if (params.cc) {
|
|
97
|
+
console.log(
|
|
98
|
+
`CC: ${Array.isArray(params.cc) ? params.cc.join(", ") : params.cc}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (params.bcc) {
|
|
102
|
+
console.log(
|
|
103
|
+
`BCC: ${Array.isArray(params.bcc) ? params.bcc.join(", ") : params.bcc}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (params.replyTo) {
|
|
107
|
+
console.log(
|
|
108
|
+
`Reply: ${Array.isArray(params.replyTo) ? params.replyTo.join(", ") : params.replyTo}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
console.log("-".repeat(60));
|
|
112
|
+
console.log("💡 Set force: true to send real email in development");
|
|
113
|
+
console.log(`${"=".repeat(60)}\n`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================
|
|
117
|
+
// 核心函数
|
|
118
|
+
// ============================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 发送邮件
|
|
122
|
+
*
|
|
123
|
+
* 主要特性:
|
|
124
|
+
* - 开发环境默认只输出日志,不发送真实邮件
|
|
125
|
+
* - 通过 force: true 可在开发环境发送真实邮件
|
|
126
|
+
* - 生产环境始终发送真实邮件
|
|
127
|
+
* - 完善的错误处理
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* // 发送欢迎邮件
|
|
132
|
+
* await sendEmail({
|
|
133
|
+
* to: "user@example.com",
|
|
134
|
+
* subject: "Welcome to NextDevTpl!",
|
|
135
|
+
* react: <WelcomeEmail name="John" dashboardUrl="..." />,
|
|
136
|
+
* });
|
|
137
|
+
*
|
|
138
|
+
* // 开发环境强制发送
|
|
139
|
+
* await sendEmail({
|
|
140
|
+
* to: "user@example.com",
|
|
141
|
+
* subject: "Test Email",
|
|
142
|
+
* react: <TestEmail />,
|
|
143
|
+
* force: true,
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export async function sendEmail(
|
|
148
|
+
params: SendEmailParams
|
|
149
|
+
): Promise<SendEmailResult> {
|
|
150
|
+
const { to, subject, react, from, cc, bcc, replyTo, force = false } = params;
|
|
151
|
+
|
|
152
|
+
// 开发环境且未强制发送 -> 模拟发送
|
|
153
|
+
if (isDevelopment() && !force) {
|
|
154
|
+
logEmailPreview(params);
|
|
155
|
+
return {
|
|
156
|
+
success: true,
|
|
157
|
+
simulated: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 真实发送邮件
|
|
162
|
+
try {
|
|
163
|
+
const html = await render(react);
|
|
164
|
+
const result = await mailService.send({
|
|
165
|
+
from: parseMailAddress(from ?? DEFAULT_FROM_EMAIL),
|
|
166
|
+
to: Array.isArray(to) ? to : [to],
|
|
167
|
+
subject,
|
|
168
|
+
html,
|
|
169
|
+
text: toPlainText(html),
|
|
170
|
+
...(cc ? { cc: Array.isArray(cc) ? cc : [cc] } : {}),
|
|
171
|
+
...(bcc ? { bcc: Array.isArray(bcc) ? bcc : [bcc] } : {}),
|
|
172
|
+
...(replyTo
|
|
173
|
+
? { replyTo: Array.isArray(replyTo) ? replyTo : [replyTo] }
|
|
174
|
+
: {}),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
...(result.id ? { id: result.id } : {}),
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const errorMessage =
|
|
183
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
184
|
+
console.error("Email sending error:", errorMessage);
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
error: errorMessage,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion, useSpring, useTransform } from "framer-motion";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 动画价格组件属性
|
|
8
|
+
*/
|
|
9
|
+
interface AnimatedPriceProps {
|
|
10
|
+
/** 目标价格值 */
|
|
11
|
+
value: number;
|
|
12
|
+
/** 自定义类名 */
|
|
13
|
+
className?: string;
|
|
14
|
+
/** 数字格式化选项 */
|
|
15
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 动画价格组件
|
|
20
|
+
*
|
|
21
|
+
* 使用 Framer Motion 实现价格数字的平滑跳动效果
|
|
22
|
+
* 当价格变化时,数字会以弹簧动画的方式过渡到新值
|
|
23
|
+
*/
|
|
24
|
+
export function AnimatedPrice({
|
|
25
|
+
value,
|
|
26
|
+
className,
|
|
27
|
+
formatOptions,
|
|
28
|
+
}: AnimatedPriceProps) {
|
|
29
|
+
// 使用 spring 动画实现平滑过渡
|
|
30
|
+
const spring = useSpring(value, {
|
|
31
|
+
stiffness: 100,
|
|
32
|
+
damping: 20,
|
|
33
|
+
mass: 1,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 将 spring 值转换为显示格式
|
|
37
|
+
const display = useTransform(spring, (latest) => {
|
|
38
|
+
const rounded = Math.round(latest);
|
|
39
|
+
if (formatOptions) {
|
|
40
|
+
return new Intl.NumberFormat("en-US", formatOptions).format(rounded);
|
|
41
|
+
}
|
|
42
|
+
return String(rounded);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 当 value 变化时更新 spring 目标值
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
spring.set(value);
|
|
48
|
+
}, [spring, value]);
|
|
49
|
+
|
|
50
|
+
return <motion.span className={className}>{display}</motion.span>;
|
|
51
|
+
}
|