create-nextdevtpl 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +29 -0
  2. package/catalog.json +385 -0
  3. package/dist/catalog.d.ts +3 -0
  4. package/dist/catalog.js +112 -0
  5. package/dist/catalog.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.js +115 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/filesystem.d.ts +10 -0
  10. package/dist/filesystem.js +86 -0
  11. package/dist/filesystem.js.map +1 -0
  12. package/dist/generate.d.ts +5 -0
  13. package/dist/generate.js +758 -0
  14. package/dist/generate.js.map +1 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/selection.d.ts +2 -0
  19. package/dist/selection.js +111 -0
  20. package/dist/selection.js.map +1 -0
  21. package/dist/service-templates.d.ts +4 -0
  22. package/dist/service-templates.js +180 -0
  23. package/dist/service-templates.js.map +1 -0
  24. package/dist/types.d.ts +65 -0
  25. package/dist/types.js +2 -0
  26. package/dist/types.js.map +1 -0
  27. package/package.json +54 -0
  28. package/template/.dockerignore +14 -0
  29. package/template/.env.example +282 -0
  30. package/template/.gitattributes +17 -0
  31. package/template/.node-version +1 -0
  32. package/template/Dockerfile +45 -0
  33. package/template/LICENSE +21 -0
  34. package/template/README.md +267 -0
  35. package/template/biome.json +68 -0
  36. package/template/cloudflare/templates/logger.ts +162 -0
  37. package/template/cloudflare/templates/mail/server.ts +6 -0
  38. package/template/cloudflare/templates/mail/templates.ts +87 -0
  39. package/template/cloudflare/templates/mail/utils.ts +69 -0
  40. package/template/cloudflare/worker.mjs +25 -0
  41. package/template/components.json +22 -0
  42. package/template/compose.yaml +74 -0
  43. package/template/deploy/check-health.mjs +25 -0
  44. package/template/deploy/docker/README.md +14 -0
  45. package/template/deploy/server/README.md +24 -0
  46. package/template/deploy/server/build.sh +24 -0
  47. package/template/deploy/server/nextdevtpl.service +19 -0
  48. package/template/deploy/server/start.sh +19 -0
  49. package/template/deploy/vercel/README.md +16 -0
  50. package/template/deploy/verify-production-health.mjs +86 -0
  51. package/template/drizzle.config.ts +47 -0
  52. package/template/gitignore +62 -0
  53. package/template/manifest.json +35 -0
  54. package/template/messages/en.json +766 -0
  55. package/template/messages/zh.json +762 -0
  56. package/template/next.config.mjs +28 -0
  57. package/template/open-next.config.ts +3 -0
  58. package/template/package.json +125 -0
  59. package/template/pnpm-lock.yaml +15901 -0
  60. package/template/postcss.config.mjs +7 -0
  61. package/template/public/file.svg +1 -0
  62. package/template/public/globe.svg +1 -0
  63. package/template/public/logo.svg +6 -0
  64. package/template/public/next.svg +1 -0
  65. package/template/public/vercel.svg +1 -0
  66. package/template/public/window.svg +1 -0
  67. package/template/sentry.client.config.ts +10 -0
  68. package/template/sentry.edge.config.ts +10 -0
  69. package/template/sentry.server.config.ts +10 -0
  70. package/template/source.config.ts +59 -0
  71. package/template/src/adapters/ai/anthropic.ts +113 -0
  72. package/template/src/adapters/ai/index.ts +6 -0
  73. package/template/src/adapters/ai/openai-compatible.ts +88 -0
  74. package/template/src/adapters/ai/workers-ai.ts +63 -0
  75. package/template/src/adapters/index.ts +10 -0
  76. package/template/src/adapters/jobs/cloudflare-workflows.ts +46 -0
  77. package/template/src/adapters/jobs/index.ts +5 -0
  78. package/template/src/adapters/jobs/inngest/adapter.ts +29 -0
  79. package/template/src/adapters/jobs/inngest/client.ts +8 -0
  80. package/template/src/adapters/jobs/inngest/functions.ts +17 -0
  81. package/template/src/adapters/jobs/inngest/handler.ts +6 -0
  82. package/template/src/adapters/jobs/inngest/index.ts +4 -0
  83. package/template/src/adapters/mail/cloudflare-email.ts +50 -0
  84. package/template/src/adapters/mail/disabled.ts +17 -0
  85. package/template/src/adapters/mail/index.ts +7 -0
  86. package/template/src/adapters/mail/resend.ts +76 -0
  87. package/template/src/adapters/mail/smtp.ts +82 -0
  88. package/template/src/adapters/payment/creem.ts +269 -0
  89. package/template/src/adapters/payment/index.ts +2 -0
  90. package/template/src/adapters/payment/stripe.ts +302 -0
  91. package/template/src/adapters/rate-limit/cloudflare.ts +42 -0
  92. package/template/src/adapters/rate-limit/index.ts +6 -0
  93. package/template/src/adapters/rate-limit/noop.ts +27 -0
  94. package/template/src/adapters/rate-limit/upstash.ts +113 -0
  95. package/template/src/adapters/registry.ts +228 -0
  96. package/template/src/adapters/storage/index.ts +3 -0
  97. package/template/src/adapters/storage/r2-binding.ts +105 -0
  98. package/template/src/adapters/storage/s3-compatible.ts +149 -0
  99. package/template/src/app/[locale]/(admin)/admin/layout.tsx +36 -0
  100. package/template/src/app/[locale]/(admin)/admin/page.tsx +345 -0
  101. package/template/src/app/[locale]/(admin)/admin/tickets/[id]/page.tsx +261 -0
  102. package/template/src/app/[locale]/(admin)/admin/tickets/page.tsx +236 -0
  103. package/template/src/app/[locale]/(admin)/admin/users/page.tsx +557 -0
  104. package/template/src/app/[locale]/(auth)/forgot-password/page.tsx +9 -0
  105. package/template/src/app/[locale]/(auth)/layout.tsx +28 -0
  106. package/template/src/app/[locale]/(auth)/sign-in/page.tsx +9 -0
  107. package/template/src/app/[locale]/(auth)/sign-up/page.tsx +9 -0
  108. package/template/src/app/[locale]/(dashboard)/dashboard/credits/buy/buy-credits-view.tsx +170 -0
  109. package/template/src/app/[locale]/(dashboard)/dashboard/credits/buy/page.tsx +12 -0
  110. package/template/src/app/[locale]/(dashboard)/dashboard/credits/page.tsx +5 -0
  111. package/template/src/app/[locale]/(dashboard)/dashboard/loading.tsx +41 -0
  112. package/template/src/app/[locale]/(dashboard)/dashboard/page.tsx +299 -0
  113. package/template/src/app/[locale]/(dashboard)/dashboard/settings/page.tsx +38 -0
  114. package/template/src/app/[locale]/(dashboard)/dashboard/support/[id]/page.tsx +220 -0
  115. package/template/src/app/[locale]/(dashboard)/dashboard/support/new/page.tsx +183 -0
  116. package/template/src/app/[locale]/(dashboard)/dashboard/support/page.tsx +145 -0
  117. package/template/src/app/[locale]/(dashboard)/layout.tsx +35 -0
  118. package/template/src/app/[locale]/(marketing)/blog/[slug]/page.tsx +178 -0
  119. package/template/src/app/[locale]/(marketing)/blog/blog-post-card.tsx +95 -0
  120. package/template/src/app/[locale]/(marketing)/blog/page.tsx +127 -0
  121. package/template/src/app/[locale]/(marketing)/demo/plan-badges/page.tsx +36 -0
  122. package/template/src/app/[locale]/(marketing)/layout.tsx +15 -0
  123. package/template/src/app/[locale]/(marketing)/legal/[slug]/page.tsx +130 -0
  124. package/template/src/app/[locale]/(marketing)/page.tsx +94 -0
  125. package/template/src/app/[locale]/(marketing)/pseo/[slug]/page.tsx +68 -0
  126. package/template/src/app/[locale]/(marketing)/pseo/page.tsx +137 -0
  127. package/template/src/app/[locale]/docs/[[...slug]]/page.tsx +63 -0
  128. package/template/src/app/[locale]/docs/layout.tsx +39 -0
  129. package/template/src/app/[locale]/layout.tsx +80 -0
  130. package/template/src/app/[locale]/opengraph-image.tsx +20 -0
  131. package/template/src/app/[locale]/twitter-image.tsx +20 -0
  132. package/template/src/app/api/auth/[...all]/route.ts +20 -0
  133. package/template/src/app/api/health/route.ts +45 -0
  134. package/template/src/app/api/inngest/route.ts +12 -0
  135. package/template/src/app/api/jobs/credits/expire/route.ts +102 -0
  136. package/template/src/app/api/search/route.ts +17 -0
  137. package/template/src/app/api/upload/presigned/route.ts +98 -0
  138. package/template/src/app/api/webhooks/payment/route.ts +363 -0
  139. package/template/src/app/globals.css +406 -0
  140. package/template/src/app/icon.svg +6 -0
  141. package/template/src/app/image-proxy/[...path]/route.ts +137 -0
  142. package/template/src/app/layout.tsx +73 -0
  143. package/template/src/app/robots.ts +29 -0
  144. package/template/src/app/sitemap.ts +68 -0
  145. package/template/src/components/motion/reveal.tsx +49 -0
  146. package/template/src/components/seo/json-ld.tsx +87 -0
  147. package/template/src/components/seo/og-image-template.tsx +118 -0
  148. package/template/src/components/ui/accordion.tsx +66 -0
  149. package/template/src/components/ui/alert-dialog.tsx +195 -0
  150. package/template/src/components/ui/avatar.tsx +53 -0
  151. package/template/src/components/ui/badge.tsx +46 -0
  152. package/template/src/components/ui/button.tsx +65 -0
  153. package/template/src/components/ui/card.tsx +92 -0
  154. package/template/src/components/ui/checkbox.tsx +32 -0
  155. package/template/src/components/ui/collapsible.tsx +33 -0
  156. package/template/src/components/ui/dialog.tsx +124 -0
  157. package/template/src/components/ui/dropdown-menu.tsx +257 -0
  158. package/template/src/components/ui/form.tsx +233 -0
  159. package/template/src/components/ui/index.ts +2 -0
  160. package/template/src/components/ui/input.tsx +21 -0
  161. package/template/src/components/ui/label.tsx +24 -0
  162. package/template/src/components/ui/navigation-menu.tsx +167 -0
  163. package/template/src/components/ui/pagination.tsx +125 -0
  164. package/template/src/components/ui/popover.tsx +48 -0
  165. package/template/src/components/ui/progress.tsx +27 -0
  166. package/template/src/components/ui/radio-group.tsx +45 -0
  167. package/template/src/components/ui/select.tsx +190 -0
  168. package/template/src/components/ui/separator.tsx +28 -0
  169. package/template/src/components/ui/sheet.tsx +140 -0
  170. package/template/src/components/ui/switch.tsx +31 -0
  171. package/template/src/components/ui/tabs.tsx +75 -0
  172. package/template/src/components/ui/textarea.tsx +18 -0
  173. package/template/src/config/index.ts +31 -0
  174. package/template/src/config/nav.ts +238 -0
  175. package/template/src/config/payment.ts +282 -0
  176. package/template/src/config/site.ts +53 -0
  177. package/template/src/config/subscription-plan.ts +164 -0
  178. package/template/src/content/blog/en/hello-world.mdx +48 -0
  179. package/template/src/content/blog/zh/hello-world.mdx +48 -0
  180. package/template/src/content/blog/zh/nextdevtpl-linuxdo-intro.mdx +98 -0
  181. package/template/src/content/docs/en/admin-and-support.mdx +65 -0
  182. package/template/src/content/docs/en/authentication.mdx +71 -0
  183. package/template/src/content/docs/en/background-jobs.mdx +53 -0
  184. package/template/src/content/docs/en/configuration.mdx +71 -0
  185. package/template/src/content/docs/en/credits.mdx +69 -0
  186. package/template/src/content/docs/en/customization.mdx +95 -0
  187. package/template/src/content/docs/en/database.mdx +66 -0
  188. package/template/src/content/docs/en/deployment.mdx +62 -0
  189. package/template/src/content/docs/en/email.mdx +48 -0
  190. package/template/src/content/docs/en/index.mdx +48 -0
  191. package/template/src/content/docs/en/internationalization.mdx +78 -0
  192. package/template/src/content/docs/en/meta.json +22 -0
  193. package/template/src/content/docs/en/observability.mdx +69 -0
  194. package/template/src/content/docs/en/payments.mdx +64 -0
  195. package/template/src/content/docs/en/project-structure.mdx +82 -0
  196. package/template/src/content/docs/en/quick-start.mdx +113 -0
  197. package/template/src/content/docs/en/storage.mdx +51 -0
  198. package/template/src/content/docs/zh/admin-and-support.mdx +65 -0
  199. package/template/src/content/docs/zh/authentication.mdx +71 -0
  200. package/template/src/content/docs/zh/background-jobs.mdx +53 -0
  201. package/template/src/content/docs/zh/configuration.mdx +70 -0
  202. package/template/src/content/docs/zh/credits.mdx +69 -0
  203. package/template/src/content/docs/zh/customization.mdx +95 -0
  204. package/template/src/content/docs/zh/database.mdx +66 -0
  205. package/template/src/content/docs/zh/deployment.mdx +61 -0
  206. package/template/src/content/docs/zh/email.mdx +47 -0
  207. package/template/src/content/docs/zh/index.mdx +48 -0
  208. package/template/src/content/docs/zh/internationalization.mdx +78 -0
  209. package/template/src/content/docs/zh/meta.json +22 -0
  210. package/template/src/content/docs/zh/observability.mdx +68 -0
  211. package/template/src/content/docs/zh/payments.mdx +64 -0
  212. package/template/src/content/docs/zh/project-structure.mdx +82 -0
  213. package/template/src/content/docs/zh/quick-start.mdx +110 -0
  214. package/template/src/content/docs/zh/storage.mdx +51 -0
  215. package/template/src/content/legal/en/cookie-policy.mdx +78 -0
  216. package/template/src/content/legal/en/privacy.mdx +131 -0
  217. package/template/src/content/legal/en/terms.mdx +93 -0
  218. package/template/src/content/legal/zh/cookie-policy.mdx +78 -0
  219. package/template/src/content/legal/zh/privacy.mdx +131 -0
  220. package/template/src/content/legal/zh/terms.mdx +92 -0
  221. package/template/src/core/modules/contract.ts +225 -0
  222. package/template/src/core/modules/index.ts +1 -0
  223. package/template/src/core/services/ai.ts +31 -0
  224. package/template/src/core/services/common.ts +112 -0
  225. package/template/src/core/services/index.ts +8 -0
  226. package/template/src/core/services/jobs.ts +24 -0
  227. package/template/src/core/services/mail.ts +35 -0
  228. package/template/src/core/services/manifest.ts +51 -0
  229. package/template/src/core/services/payment.ts +92 -0
  230. package/template/src/core/services/rate-limit.ts +47 -0
  231. package/template/src/core/services/storage.ts +42 -0
  232. package/template/src/db/index.ts +71 -0
  233. package/template/src/db/schema/auth.ts +67 -0
  234. package/template/src/db/schema/credits.ts +94 -0
  235. package/template/src/db/schema/index.ts +5 -0
  236. package/template/src/db/schema/mail.ts +14 -0
  237. package/template/src/db/schema/subscription.ts +21 -0
  238. package/template/src/db/schema/support.ts +56 -0
  239. package/template/src/db/schema.ts +2 -0
  240. package/template/src/features/admin/components/admin-sidebar.tsx +213 -0
  241. package/template/src/features/admin/components/index.ts +2 -0
  242. package/template/src/features/admin/index.ts +3 -0
  243. package/template/src/features/admin/manifest.ts +41 -0
  244. package/template/src/features/analytics/components/analytics.tsx +62 -0
  245. package/template/src/features/analytics/components/index.ts +2 -0
  246. package/template/src/features/analytics/index.ts +3 -0
  247. package/template/src/features/analytics/manifest.ts +13 -0
  248. package/template/src/features/auth/components/auth-brand-panel.tsx +82 -0
  249. package/template/src/features/auth/components/auth-error-alert.tsx +28 -0
  250. package/template/src/features/auth/components/auth-footer.tsx +52 -0
  251. package/template/src/features/auth/components/auth-logo.tsx +30 -0
  252. package/template/src/features/auth/components/forgot-password-form.tsx +146 -0
  253. package/template/src/features/auth/components/index.ts +9 -0
  254. package/template/src/features/auth/components/sign-in-form.tsx +241 -0
  255. package/template/src/features/auth/components/sign-up-form.tsx +307 -0
  256. package/template/src/features/auth/index.ts +3 -0
  257. package/template/src/features/auth/manifest.ts +30 -0
  258. package/template/src/features/blog/components/blog-post-item.tsx +72 -0
  259. package/template/src/features/blog/components/index.ts +1 -0
  260. package/template/src/features/blog/data/mock-posts.ts +62 -0
  261. package/template/src/features/blog/index.ts +3 -0
  262. package/template/src/features/blog/manifest.ts +22 -0
  263. package/template/src/features/credits/account.ts +76 -0
  264. package/template/src/features/credits/actions.ts +427 -0
  265. package/template/src/features/credits/components/credit-balance-badge.tsx +54 -0
  266. package/template/src/features/credits/components/credit-usage-section.tsx +160 -0
  267. package/template/src/features/credits/components/index.ts +7 -0
  268. package/template/src/features/credits/components/transaction-history.tsx +346 -0
  269. package/template/src/features/credits/config.ts +70 -0
  270. package/template/src/features/credits/consume.ts +140 -0
  271. package/template/src/features/credits/core.ts +23 -0
  272. package/template/src/features/credits/errors.ts +26 -0
  273. package/template/src/features/credits/expire.ts +112 -0
  274. package/template/src/features/credits/grant.ts +168 -0
  275. package/template/src/features/credits/index.ts +6 -0
  276. package/template/src/features/credits/manifest.ts +36 -0
  277. package/template/src/features/credits/query.ts +65 -0
  278. package/template/src/features/credits/types.ts +48 -0
  279. package/template/src/features/dashboard/components/index.ts +3 -0
  280. package/template/src/features/dashboard/components/main-wrapper.tsx +100 -0
  281. package/template/src/features/dashboard/components/sidebar.tsx +341 -0
  282. package/template/src/features/dashboard/context/index.ts +2 -0
  283. package/template/src/features/dashboard/context/sidebar-context.tsx +72 -0
  284. package/template/src/features/dashboard/index.ts +4 -0
  285. package/template/src/features/dashboard/manifest.ts +28 -0
  286. package/template/src/features/mail/actions.ts +175 -0
  287. package/template/src/features/mail/index.ts +5 -0
  288. package/template/src/features/mail/manifest.ts +13 -0
  289. package/template/src/features/mail/server.ts +8 -0
  290. package/template/src/features/mail/templates/index.ts +11 -0
  291. package/template/src/features/mail/templates/primary-action-email.tsx +224 -0
  292. package/template/src/features/mail/templates/welcome-email.tsx +121 -0
  293. package/template/src/features/mail/utils.ts +190 -0
  294. package/template/src/features/marketing/components/animated-price.tsx +51 -0
  295. package/template/src/features/marketing/components/cookie-consent.tsx +255 -0
  296. package/template/src/features/marketing/components/cta-section.tsx +50 -0
  297. package/template/src/features/marketing/components/faq-section.tsx +59 -0
  298. package/template/src/features/marketing/components/feature-grid.tsx +80 -0
  299. package/template/src/features/marketing/components/footer.tsx +128 -0
  300. package/template/src/features/marketing/components/header.tsx +255 -0
  301. package/template/src/features/marketing/components/hero-section.tsx +323 -0
  302. package/template/src/features/marketing/components/how-it-works.tsx +115 -0
  303. package/template/src/features/marketing/components/index.ts +14 -0
  304. package/template/src/features/marketing/components/nav-menu.tsx +236 -0
  305. package/template/src/features/marketing/components/pricing-section.tsx +446 -0
  306. package/template/src/features/marketing/components/testimonials.tsx +59 -0
  307. package/template/src/features/marketing/components/use-cases-section.tsx +78 -0
  308. package/template/src/features/marketing/index.ts +3 -0
  309. package/template/src/features/marketing/manifest.ts +35 -0
  310. package/template/src/features/payment/actions.ts +182 -0
  311. package/template/src/features/payment/index.ts +4 -0
  312. package/template/src/features/payment/manifest.ts +18 -0
  313. package/template/src/features/payment/server.ts +7 -0
  314. package/template/src/features/payment/types.ts +202 -0
  315. package/template/src/features/pseo/components/index.ts +6 -0
  316. package/template/src/features/pseo/components/pseo-cta.tsx +51 -0
  317. package/template/src/features/pseo/components/pseo-faq.tsx +40 -0
  318. package/template/src/features/pseo/components/pseo-feature-grid.tsx +74 -0
  319. package/template/src/features/pseo/components/pseo-hero.tsx +106 -0
  320. package/template/src/features/pseo/components/pseo-related.tsx +62 -0
  321. package/template/src/features/pseo/components/pseo-use-cases.tsx +50 -0
  322. package/template/src/features/pseo/data/pseo-pages.json +534 -0
  323. package/template/src/features/pseo/index.ts +4 -0
  324. package/template/src/features/pseo/lib/pseo-data.ts +140 -0
  325. package/template/src/features/pseo/manifest.ts +22 -0
  326. package/template/src/features/settings/actions/delete-account.ts +50 -0
  327. package/template/src/features/settings/actions/index.ts +3 -0
  328. package/template/src/features/settings/actions/update-profile.ts +49 -0
  329. package/template/src/features/settings/components/billing-section.tsx +283 -0
  330. package/template/src/features/settings/components/index.ts +3 -0
  331. package/template/src/features/settings/components/profile-form.tsx +154 -0
  332. package/template/src/features/settings/components/security-section.tsx +158 -0
  333. package/template/src/features/settings/components/settings-profile-view.tsx +506 -0
  334. package/template/src/features/settings/index.ts +5 -0
  335. package/template/src/features/settings/manifest.ts +28 -0
  336. package/template/src/features/settings/schemas/index.ts +2 -0
  337. package/template/src/features/settings/schemas/update-profile.ts +21 -0
  338. package/template/src/features/shared/components/cookie-settings-dialog.tsx +282 -0
  339. package/template/src/features/shared/components/index.ts +5 -0
  340. package/template/src/features/shared/components/language-switcher.tsx +82 -0
  341. package/template/src/features/shared/components/max-width-wrapper.tsx +49 -0
  342. package/template/src/features/shared/components/mode-toggle.tsx +119 -0
  343. package/template/src/features/shared/icons/google-icon.tsx +39 -0
  344. package/template/src/features/shared/icons/index.ts +7 -0
  345. package/template/src/features/shared/index.ts +5 -0
  346. package/template/src/features/shared/manifest.ts +13 -0
  347. package/template/src/features/shared/providers.tsx +59 -0
  348. package/template/src/features/storage/actions.ts +107 -0
  349. package/template/src/features/storage/index.ts +6 -0
  350. package/template/src/features/storage/manifest.ts +22 -0
  351. package/template/src/features/storage/types.ts +143 -0
  352. package/template/src/features/storage/utils.ts +85 -0
  353. package/template/src/features/storage/validation.ts +151 -0
  354. package/template/src/features/subscription/actions/get-user-plan.ts +26 -0
  355. package/template/src/features/subscription/actions/index.ts +5 -0
  356. package/template/src/features/subscription/components/current-plan-badge.tsx +18 -0
  357. package/template/src/features/subscription/components/index.ts +6 -0
  358. package/template/src/features/subscription/components/plan-badge.tsx +98 -0
  359. package/template/src/features/subscription/index.ts +25 -0
  360. package/template/src/features/subscription/manifest.ts +13 -0
  361. package/template/src/features/subscription/services/user-plan.ts +181 -0
  362. package/template/src/features/support/actions/admin-ticket.ts +172 -0
  363. package/template/src/features/support/actions/admin-users.ts +295 -0
  364. package/template/src/features/support/actions/index.ts +22 -0
  365. package/template/src/features/support/actions/ticket.ts +12 -0
  366. package/template/src/features/support/actions/user-ticket.ts +153 -0
  367. package/template/src/features/support/components/admin-ticket-reply-form.tsx +109 -0
  368. package/template/src/features/support/components/admin-ticket-status-select.tsx +119 -0
  369. package/template/src/features/support/components/index.ts +6 -0
  370. package/template/src/features/support/components/ticket-message-form.tsx +99 -0
  371. package/template/src/features/support/components/user-role-select.tsx +107 -0
  372. package/template/src/features/support/index.ts +5 -0
  373. package/template/src/features/support/manifest.ts +53 -0
  374. package/template/src/features/support/schemas/index.ts +15 -0
  375. package/template/src/features/support/schemas/ticket.ts +81 -0
  376. package/template/src/hooks/index.ts +2 -0
  377. package/template/src/i18n/index.ts +12 -0
  378. package/template/src/i18n/request.ts +24 -0
  379. package/template/src/i18n/routing.ts +23 -0
  380. package/template/src/instrumentation.ts +19 -0
  381. package/template/src/lib/ai/index.ts +16 -0
  382. package/template/src/lib/api-logger.ts +24 -0
  383. package/template/src/lib/auth/admin.ts +54 -0
  384. package/template/src/lib/auth/api.ts +17 -0
  385. package/template/src/lib/auth/client.ts +227 -0
  386. package/template/src/lib/auth/edge.ts +84 -0
  387. package/template/src/lib/auth/errors.ts +99 -0
  388. package/template/src/lib/auth/index.ts +159 -0
  389. package/template/src/lib/auth/server.ts +46 -0
  390. package/template/src/lib/auth/session-context.tsx +96 -0
  391. package/template/src/lib/cloudflare/bindings.ts +31 -0
  392. package/template/src/lib/cookie-consent.ts +33 -0
  393. package/template/src/lib/file-utils.ts +45 -0
  394. package/template/src/lib/fingerprint/index.ts +73 -0
  395. package/template/src/lib/health/checks.ts +6 -0
  396. package/template/src/lib/health/core.ts +86 -0
  397. package/template/src/lib/health/index.ts +11 -0
  398. package/template/src/lib/health/types.ts +13 -0
  399. package/template/src/lib/index.ts +2 -0
  400. package/template/src/lib/logger/index.ts +258 -0
  401. package/template/src/lib/monitoring/index.ts +384 -0
  402. package/template/src/lib/rate-limit/anonymous.ts +85 -0
  403. package/template/src/lib/rate-limit/index.ts +111 -0
  404. package/template/src/lib/safe-action.ts +127 -0
  405. package/template/src/lib/seo/json-ld.ts +188 -0
  406. package/template/src/lib/source.ts +122 -0
  407. package/template/src/lib/utils.ts +6 -0
  408. package/template/src/middleware.ts +162 -0
  409. package/template/src/modules/index.ts +10 -0
  410. package/template/src/modules/presets.ts +8 -0
  411. package/template/src/modules/registry.ts +34 -0
  412. package/template/src/modules/selection.ts +45 -0
  413. package/template/src/services/ai.ts +49 -0
  414. package/template/src/services/index.ts +6 -0
  415. package/template/src/services/jobs.ts +3 -0
  416. package/template/src/services/mail.ts +7 -0
  417. package/template/src/services/payment.ts +3 -0
  418. package/template/src/services/rate-limit.ts +12 -0
  419. package/template/src/services/storage.ts +3 -0
  420. package/template/src/types/fumadocs-source.d.ts +14 -0
  421. package/template/src/types/index.ts +2 -0
  422. package/template/tsconfig.json +53 -0
  423. package/template/vercel.json +11 -0
  424. package/template/vitest.config.ts +51 -0
  425. package/template/wrangler.jsonc +30 -0
@@ -0,0 +1,107 @@
1
+ "use server";
2
+
3
+ /**
4
+ * 存储系统 Server Actions
5
+ *
6
+ * 提供文件上传和管理的服务端操作接口
7
+ */
8
+
9
+ import { z } from "zod";
10
+
11
+ import { protectedAction } from "@/lib/safe-action";
12
+ import { storageService } from "@/services/storage";
13
+
14
+ import {
15
+ ALLOWED_IMAGE_TYPES,
16
+ DEFAULT_UPLOAD_URL_EXPIRES,
17
+ type AllowedImageType,
18
+ } from "./types";
19
+ import { validateUploadRequest } from "./validation";
20
+
21
+ const withStorageAction = (name: string) =>
22
+ protectedAction.metadata({ action: `storage.${name}` });
23
+
24
+ // ============================================
25
+ // 存储配置
26
+ // ============================================
27
+
28
+ /**
29
+ * 获取头像存储桶名称
30
+ */
31
+ function getAvatarsBucket(): string {
32
+ const bucket = process.env.NEXT_PUBLIC_AVATARS_BUCKET_NAME;
33
+ if (!bucket) {
34
+ throw new Error("NEXT_PUBLIC_AVATARS_BUCKET_NAME 环境变量未设置");
35
+ }
36
+ return bucket;
37
+ }
38
+
39
+ /**
40
+ * 允许的存储桶白名单
41
+ *
42
+ * 安全措施:只允许访问预定义的存储桶
43
+ */
44
+ function getAllowedBuckets(): string[] {
45
+ return [getAvatarsBucket()];
46
+ }
47
+
48
+ // ============================================
49
+ // 上传 URL Actions
50
+ // ============================================
51
+
52
+ /**
53
+ * 获取签名上传 URL
54
+ *
55
+ * 受保护的 Action - 需要用户登录
56
+ * 客户端使用此 URL 直接上传文件到存储
57
+ */
58
+ export const getSignedUploadUrlAction = withStorageAction("getSignedUploadUrl")
59
+ .schema(
60
+ z.object({
61
+ /** 文件键名 (规范格式为 `${userId}/...`,如 user-123/169.jpg) */
62
+ key: z.string().min(1, "文件键名不能为空").max(255, "文件键名过长"),
63
+ /** 文件 MIME 类型 */
64
+ contentType: z.enum(
65
+ ["image/jpeg", "image/png", "image/gif", "image/webp"],
66
+ {
67
+ message: `只支持以下文件类型: ${ALLOWED_IMAGE_TYPES.join(", ")}`,
68
+ }
69
+ ),
70
+ /** 存储桶名称 (可选,默认使用头像桶) */
71
+ bucket: z.string().optional(),
72
+ })
73
+ )
74
+ .action(async ({ parsedInput, ctx }) => {
75
+ const { key, contentType, bucket: inputBucket } = parsedInput;
76
+ const { userId } = ctx;
77
+
78
+ // 确定使用的存储桶
79
+ const bucket = inputBucket ?? getAvatarsBucket();
80
+
81
+ // 安全检查:存储桶白名单 + 键名格式(含路径遍历防护) + 文件归属(锚定 userId)
82
+ // 校验逻辑集中在 ./validation,测试直接 import 同一份函数,避免漂移
83
+ const validation = validateUploadRequest({
84
+ key,
85
+ bucket,
86
+ contentType,
87
+ userId,
88
+ allowedBuckets: getAllowedBuckets(),
89
+ });
90
+ if (!validation.valid) {
91
+ throw new Error(validation.error);
92
+ }
93
+
94
+ // 获取签名上传 URL
95
+ const uploadUrl = await storageService.createUploadUrl({
96
+ bucket,
97
+ key,
98
+ contentType: contentType as AllowedImageType,
99
+ expiresIn: DEFAULT_UPLOAD_URL_EXPIRES,
100
+ });
101
+
102
+ return {
103
+ uploadUrl,
104
+ key,
105
+ bucket,
106
+ };
107
+ });
@@ -0,0 +1,6 @@
1
+ // Storage System
2
+ export * from "./actions";
3
+ export * from "./types";
4
+ export * from "./utils";
5
+ export * from "./validation";
6
+ export { storageModule } from "./manifest";
@@ -0,0 +1,22 @@
1
+ import { defineModule } from "@/core/modules";
2
+
3
+ export const storageModule = defineModule({
4
+ id: "storage",
5
+ name: "Storage",
6
+ description: "文件上传、下载和对象存储访问",
7
+ kind: "optional",
8
+ dependencies: [],
9
+ routes: [
10
+ {
11
+ path: "/api/upload/presigned",
12
+ source: "src/app/api/upload/presigned/route.ts",
13
+ },
14
+ {
15
+ path: "/image-proxy/[...path]",
16
+ source: "src/app/image-proxy/[...path]/route.ts",
17
+ },
18
+ ],
19
+ navigation: [],
20
+ translations: [],
21
+ schema: [],
22
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * 存储系统类型定义
3
+ *
4
+ * 定义存储提供者接口和相关类型
5
+ */
6
+
7
+ // ============================================
8
+ // 存储提供者接口
9
+ // ============================================
10
+
11
+ /**
12
+ * 存储提供者接口
13
+ *
14
+ * 所有存储后端 (S3, R2, MinIO 等) 都需要实现此接口
15
+ */
16
+ export interface StorageProvider {
17
+ /**
18
+ * 获取签名读取 URL
19
+ *
20
+ * 用于安全地读取存储中的文件
21
+ *
22
+ * @param key - 文件键名 (路径)
23
+ * @param bucket - 存储桶名称
24
+ * @param expiresIn - URL 有效期 (秒)
25
+ * @returns 签名后的 URL
26
+ */
27
+ getSignedUrl(key: string, bucket: string, expiresIn: number): Promise<string>;
28
+
29
+ /**
30
+ * 获取签名上传 URL
31
+ *
32
+ * 用于客户端直接上传文件到存储
33
+ *
34
+ * @param key - 文件键名 (路径)
35
+ * @param bucket - 存储桶名称
36
+ * @param contentType - 文件 MIME 类型
37
+ * @param expiresIn - URL 有效期 (秒,可选,默认 300)
38
+ * @returns 签名后的上传 URL
39
+ */
40
+ getSignedUploadUrl(
41
+ key: string,
42
+ bucket: string,
43
+ contentType: string,
44
+ expiresIn?: number
45
+ ): Promise<string>;
46
+
47
+ /**
48
+ * 删除文件
49
+ *
50
+ * @param key - 文件键名 (路径)
51
+ * @param bucket - 存储桶名称
52
+ */
53
+ deleteObject(key: string, bucket: string): Promise<void>;
54
+
55
+ /**
56
+ * 获取文件内容
57
+ *
58
+ * @param key - 文件键名 (路径)
59
+ * @param bucket - 存储桶名称
60
+ * @returns 文件内容 Buffer
61
+ */
62
+ getObject(key: string, bucket: string): Promise<Buffer>;
63
+ }
64
+
65
+ // ============================================
66
+ // 存储配置类型
67
+ // ============================================
68
+
69
+ /**
70
+ * S3 兼容存储配置
71
+ */
72
+ export interface S3StorageConfig {
73
+ /** 访问密钥 ID */
74
+ accessKeyId: string;
75
+ /** 访问密钥 */
76
+ secretAccessKey: string;
77
+ /** 端点 URL (如 Cloudflare R2, MinIO) */
78
+ endpoint: string;
79
+ /** 区域 (如 auto, us-east-1) */
80
+ region: string;
81
+ }
82
+
83
+ // ============================================
84
+ // 上传相关类型
85
+ // ============================================
86
+
87
+ /**
88
+ * 上传 URL 请求参数
89
+ */
90
+ export interface GetUploadUrlParams {
91
+ /** 文件键名 (完整路径,如 avatars/user-123.jpg) */
92
+ key: string;
93
+ /** 文件 MIME 类型 */
94
+ contentType: string;
95
+ /** 存储桶名称 (可选,默认使用头像桶) */
96
+ bucket?: string;
97
+ }
98
+
99
+ /**
100
+ * 上传 URL 响应
101
+ */
102
+ export interface GetUploadUrlResult {
103
+ /** 签名上传 URL */
104
+ uploadUrl: string;
105
+ /** 文件键名 */
106
+ key: string;
107
+ /** 存储桶名称 */
108
+ bucket: string;
109
+ }
110
+
111
+ // ============================================
112
+ // 允许的文件类型
113
+ // ============================================
114
+
115
+ /**
116
+ * 允许上传的图片 MIME 类型
117
+ */
118
+ export const ALLOWED_IMAGE_TYPES = [
119
+ "image/jpeg",
120
+ "image/png",
121
+ "image/gif",
122
+ "image/webp",
123
+ ] as const;
124
+
125
+ /**
126
+ * 允许的图片类型
127
+ */
128
+ export type AllowedImageType = (typeof ALLOWED_IMAGE_TYPES)[number];
129
+
130
+ /**
131
+ * 最大文件大小 (5MB)
132
+ */
133
+ export const MAX_FILE_SIZE = 5 * 1024 * 1024;
134
+
135
+ /**
136
+ * 默认签名 URL 有效期 (秒)
137
+ */
138
+ export const DEFAULT_SIGNED_URL_EXPIRES = 3600; // 1 小时
139
+
140
+ /**
141
+ * 默认上传 URL 有效期 (秒)
142
+ */
143
+ export const DEFAULT_UPLOAD_URL_EXPIRES = 300; // 5 分钟
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 存储 URL 工具函数
3
+ *
4
+ * 处理存储键名和外部 URL 的转换
5
+ */
6
+
7
+ // ============================================
8
+ // 头像 URL 工具
9
+ // ============================================
10
+
11
+ /**
12
+ * 判断是否为外部 URL
13
+ *
14
+ * 外部 URL 包括:
15
+ * - OAuth 提供的头像 (GitHub, Google 等)
16
+ * - 其他完整 URL
17
+ *
18
+ * @param value - URL 或存储键名
19
+ * @returns 是否为外部 URL
20
+ */
21
+ export function isExternalUrl(value: string | null | undefined): boolean {
22
+ if (!value) return false;
23
+ return value.startsWith("http://") || value.startsWith("https://");
24
+ }
25
+
26
+ /**
27
+ * 获取头像显示 URL
28
+ *
29
+ * 根据传入的值返回正确的 URL:
30
+ * - 如果是外部 URL (http/https 开头),直接返回
31
+ * - 如果是存储键名,转换为 image-proxy URL
32
+ * - 如果为空,返回 undefined
33
+ *
34
+ * @param image - 用户的 image 字段值 (可能是 URL 或存储键名)
35
+ * @returns 头像显示 URL 或 undefined
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * // 外部 URL (OAuth 头像)
40
+ * getAvatarUrl("https://avatars.githubusercontent.com/u/12345")
41
+ * // => "https://avatars.githubusercontent.com/u/12345"
42
+ *
43
+ * // 存储键名
44
+ * getAvatarUrl("user-abc123/1234567890.jpg")
45
+ * // => "/image-proxy/avatars/user-abc123/1234567890.jpg"
46
+ *
47
+ * // 空值
48
+ * getAvatarUrl(null) // => undefined
49
+ * ```
50
+ */
51
+ export function getAvatarUrl(
52
+ image: string | null | undefined
53
+ ): string | undefined {
54
+ if (!image) {
55
+ return undefined;
56
+ }
57
+
58
+ // 如果是外部 URL,直接返回
59
+ if (isExternalUrl(image)) {
60
+ return image;
61
+ }
62
+
63
+ // 否则是存储键名,转换为 image-proxy URL
64
+ const avatarsBucket =
65
+ process.env.NEXT_PUBLIC_AVATARS_BUCKET_NAME ?? "avatars";
66
+ return `/image-proxy/${avatarsBucket}/${image}`;
67
+ }
68
+
69
+ /**
70
+ * 生成唯一的头像文件键名
71
+ *
72
+ * 格式: {userId}/{timestamp}.{extension}
73
+ *
74
+ * 将 userId 作为**首个路径段**,使服务端可锚定校验文件归属
75
+ * (参见 validation.ts 的 isKeyOwnedByUser),避免子串伪造攻击。
76
+ *
77
+ * @param userId - 用户 ID
78
+ * @param file - 上传的文件
79
+ * @returns 唯一的文件键名
80
+ */
81
+ export function generateAvatarKey(userId: string, file: File): string {
82
+ const timestamp = Date.now();
83
+ const extension = file.name.split(".").pop()?.toLowerCase() ?? "jpg";
84
+ return `${userId}/${timestamp}.${extension}`;
85
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * 存储安全校验
3
+ *
4
+ * 这些是纯函数,既被 storage/actions.ts 在生产路径中调用,
5
+ * 也被 src/test/storage/security.test.ts 直接导入测试 ——
6
+ * 单一事实来源,避免测试与生产逻辑漂移。
7
+ */
8
+
9
+ import { ALLOWED_IMAGE_TYPES, type AllowedImageType } from "./types";
10
+
11
+ /** 文件键名允许的字符集(字母、数字、连字符、下划线、斜杠、点) */
12
+ export const STORAGE_KEY_REGEX = /^[a-zA-Z0-9\-_/.]+$/;
13
+
14
+ /** 文件键名最大长度 */
15
+ export const MAX_KEY_LENGTH = 255;
16
+
17
+ /**
18
+ * 校验文件键名格式
19
+ *
20
+ * 安全检查:
21
+ * - 长度 1-255
22
+ * - 显式拦截路径遍历 `..`(注意: 字符正则允许 `.`,故必须单独拦截 `..`)
23
+ * - 禁止以 `/` 开头/结尾、禁止空路径段 `//`
24
+ * - 仅允许白名单字符
25
+ */
26
+ export function validateKeyFormat(key: string): boolean {
27
+ if (key.length < 1 || key.length > MAX_KEY_LENGTH) {
28
+ return false;
29
+ }
30
+ // 路径遍历防护:正则允许单个 '.','..' 会通过正则,必须显式拦截
31
+ if (key.includes("..")) {
32
+ return false;
33
+ }
34
+ // 禁止绝对路径、尾随斜杠与空路径段
35
+ if (key.startsWith("/") || key.endsWith("/") || key.includes("//")) {
36
+ return false;
37
+ }
38
+ return STORAGE_KEY_REGEX.test(key);
39
+ }
40
+
41
+ /**
42
+ * 校验文件归属
43
+ *
44
+ * 与旧实现 `key.includes(userId)` 的区别:这里要求 userId 必须是键名的
45
+ * **首个路径段**(锚定匹配),而非任意位置的子串。这样可以阻止:
46
+ * - 子串伪造:`evil-${userId}/x.jpg`、`${userId}extra/x.jpg`
47
+ * - 跨用户访问:`other-user/${userId}-decoy/x.jpg`
48
+ *
49
+ * 规范键名格式为 `${userId}/...`(参见 utils.ts 的 generateAvatarKey)。
50
+ */
51
+ export function isKeyOwnedByUser(key: string, userId: string): boolean {
52
+ if (!userId || !key) {
53
+ return false;
54
+ }
55
+ const segments = key.split("/");
56
+ const [firstSegment, ...rest] = segments;
57
+ // 首段必须严格等于 userId,且其后至少还有一个非空文件段
58
+ return firstSegment === userId && rest.length > 0 && rest.every(Boolean);
59
+ }
60
+
61
+ /**
62
+ * 校验存储桶是否在白名单中
63
+ */
64
+ export function validateBucket(
65
+ bucket: string,
66
+ allowedBuckets: string[]
67
+ ): boolean {
68
+ return allowedBuckets.includes(bucket);
69
+ }
70
+
71
+ /**
72
+ * 校验 ContentType 是否为允许的图片类型
73
+ */
74
+ export function validateContentType(
75
+ contentType: string
76
+ ): contentType is AllowedImageType {
77
+ return (ALLOWED_IMAGE_TYPES as readonly string[]).includes(contentType);
78
+ }
79
+
80
+ /** 上传校验入参 */
81
+ export interface UploadValidationParams {
82
+ key: string;
83
+ bucket: string;
84
+ contentType: string;
85
+ userId: string;
86
+ allowedBuckets: string[];
87
+ }
88
+
89
+ /** 校验结果 */
90
+ export interface ValidationResult {
91
+ valid: boolean;
92
+ error?: string;
93
+ }
94
+
95
+ /**
96
+ * 完整校验上传请求
97
+ *
98
+ * 校验顺序: 存储桶 → 键名格式 → 文件归属 → ContentType
99
+ */
100
+ export function validateUploadRequest(
101
+ params: UploadValidationParams
102
+ ): ValidationResult {
103
+ const { key, bucket, contentType, userId, allowedBuckets } = params;
104
+
105
+ if (!validateBucket(bucket, allowedBuckets)) {
106
+ return { valid: false, error: "不允许访问该存储桶" };
107
+ }
108
+ if (!validateKeyFormat(key)) {
109
+ return { valid: false, error: "文件键名包含非法字符或长度不符" };
110
+ }
111
+ if (!isKeyOwnedByUser(key, userId)) {
112
+ return { valid: false, error: "文件路径无效:必须以用户 ID 作为前缀" };
113
+ }
114
+ if (!validateContentType(contentType)) {
115
+ return {
116
+ valid: false,
117
+ error: `只支持以下文件类型: ${ALLOWED_IMAGE_TYPES.join(", ")}`,
118
+ };
119
+ }
120
+ return { valid: true };
121
+ }
122
+
123
+ /** 删除校验入参 */
124
+ export interface DeleteValidationParams {
125
+ key: string;
126
+ bucket: string;
127
+ userId: string;
128
+ allowedBuckets: string[];
129
+ }
130
+
131
+ /**
132
+ * 完整校验删除请求
133
+ *
134
+ * 校验顺序: 存储桶 → 键名格式 → 文件归属
135
+ */
136
+ export function validateDeleteRequest(
137
+ params: DeleteValidationParams
138
+ ): ValidationResult {
139
+ const { key, bucket, userId, allowedBuckets } = params;
140
+
141
+ if (!validateBucket(bucket, allowedBuckets)) {
142
+ return { valid: false, error: "不允许访问该存储桶" };
143
+ }
144
+ if (!validateKeyFormat(key)) {
145
+ return { valid: false, error: "文件键名包含非法字符或长度不符" };
146
+ }
147
+ if (!isKeyOwnedByUser(key, userId)) {
148
+ return { valid: false, error: "无权操作此文件" };
149
+ }
150
+ return { valid: true };
151
+ }
@@ -0,0 +1,26 @@
1
+ "use server";
2
+
3
+ /**
4
+ * 获取当前用户订阅计划 Action
5
+ */
6
+
7
+ import { getUserPlan } from "@/features/subscription/services/user-plan";
8
+ import { protectedAction } from "@/lib/safe-action";
9
+
10
+ /**
11
+ * 获取当前用户的订阅计划
12
+ */
13
+ export const getMyPlanAction = protectedAction
14
+ .metadata({ action: "subscription.getMyPlan" })
15
+ .action(async ({ ctx }) => {
16
+ const userPlan = await getUserPlan(ctx.userId);
17
+
18
+ return {
19
+ plan: userPlan.plan,
20
+ planName: userPlan.planName,
21
+ hasActiveSubscription: userPlan.hasActiveSubscription,
22
+ currentPeriodEnd: userPlan.currentPeriodEnd?.toISOString() ?? null,
23
+ priceId: userPlan.priceId,
24
+ cancelAtPeriodEnd: userPlan.cancelAtPeriodEnd,
25
+ };
26
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 订阅相关 Actions 导出
3
+ */
4
+
5
+ export { getMyPlanAction } from "./get-user-plan";
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ import { useAction } from "next-safe-action/hooks";
4
+ import { useEffect } from "react";
5
+
6
+ import { getMyPlanAction } from "../actions";
7
+ import { PlanBadge, type PlanType } from "./plan-badge";
8
+
9
+ export function CurrentPlanBadge() {
10
+ const { execute: fetchPlan, result } = useAction(getMyPlanAction);
11
+
12
+ useEffect(() => {
13
+ fetchPlan();
14
+ }, [fetchPlan]);
15
+
16
+ const plan = (result.data?.plan as PlanType) || "free";
17
+ return <PlanBadge plan={plan} size="xs" />;
18
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 订阅相关组件导出
3
+ */
4
+
5
+ export { type BadgeSize, PlanBadge, type PlanType } from "./plan-badge";
6
+ export { CurrentPlanBadge } from "./current-plan-badge";
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ /**
4
+ * 订阅等级徽章组件
5
+ *
6
+ * 全部走设计系统 token,靠「中性 → 主色浅 → 金 → 主色实」的层次区分
7
+ * 四档计划,不再用紫粉/琥珀彩虹渐变 + 光晕(旧版那套是 AI 廉价感来源)。
8
+ * - free: muted(中性,最低存在感)
9
+ * - starter: primary 浅底(品牌色淡)
10
+ * - pro: warning 浅底(金色 = 高级感)
11
+ * - ultra: primary 实底(最突出)
12
+ */
13
+
14
+ import { Crown, Gem, Sparkles, User } from "lucide-react";
15
+ import { useTranslations } from "next-intl";
16
+
17
+ import { cn } from "@/lib/utils";
18
+
19
+ export type PlanType = "free" | "starter" | "pro" | "ultra";
20
+ export type BadgeSize = "xs" | "sm" | "md" | "lg";
21
+
22
+ interface PlanBadgeProps {
23
+ plan: PlanType;
24
+ size?: BadgeSize;
25
+ showLabel?: boolean;
26
+ className?: string;
27
+ }
28
+
29
+ const sizeConfig: Record<
30
+ BadgeSize,
31
+ { badge: string; icon: string; text: string }
32
+ > = {
33
+ xs: { badge: "h-5 px-1.5 gap-0.5", icon: "h-3 w-3", text: "text-[10px]" },
34
+ sm: { badge: "h-6 px-2 gap-1", icon: "h-3.5 w-3.5", text: "text-xs" },
35
+ md: { badge: "h-7 px-2.5 gap-1.5", icon: "h-4 w-4", text: "text-sm" },
36
+ lg: { badge: "h-8 px-3 gap-2", icon: "h-5 w-5", text: "text-base" },
37
+ };
38
+
39
+ const planConfig: Record<
40
+ PlanType,
41
+ { icon: typeof User; labelKey: string; baseStyles: string }
42
+ > = {
43
+ free: {
44
+ icon: User,
45
+ labelKey: "free",
46
+ baseStyles: "bg-muted text-muted-foreground",
47
+ },
48
+ starter: {
49
+ icon: Sparkles,
50
+ labelKey: "starter",
51
+ baseStyles: "bg-primary/15 text-primary",
52
+ },
53
+ pro: {
54
+ icon: Crown,
55
+ labelKey: "pro",
56
+ baseStyles: "bg-warning/15 text-warning",
57
+ },
58
+ ultra: {
59
+ icon: Gem,
60
+ labelKey: "ultra",
61
+ baseStyles: "bg-primary text-primary-foreground",
62
+ },
63
+ };
64
+
65
+ export function PlanBadge({
66
+ plan,
67
+ size = "sm",
68
+ showLabel = true,
69
+ className,
70
+ }: PlanBadgeProps) {
71
+ const t = useTranslations("Subscription");
72
+ const config = planConfig[plan];
73
+ const sizeStyles = sizeConfig[size];
74
+ const Icon = config.icon;
75
+
76
+ return (
77
+ <div
78
+ className={cn(
79
+ "inline-flex items-center justify-center rounded-full font-medium",
80
+ sizeStyles.badge,
81
+ config.baseStyles,
82
+ className
83
+ )}
84
+ >
85
+ <Icon className={sizeStyles.icon} />
86
+ {showLabel && (
87
+ <span
88
+ className={cn(
89
+ "font-semibold tracking-wide uppercase",
90
+ sizeStyles.text
91
+ )}
92
+ >
93
+ {t(`plans.${config.labelKey}`)}
94
+ </span>
95
+ )}
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 订阅功能模块
3
+ *
4
+ * 导出订阅计划相关的服务和工具函数
5
+ */
6
+
7
+ // Actions
8
+ export { getMyPlanAction } from "./actions";
9
+ // 组件
10
+ export {
11
+ type BadgeSize,
12
+ CurrentPlanBadge,
13
+ PlanBadge,
14
+ type PlanType,
15
+ } from "./components";
16
+
17
+ // 服务
18
+ export {
19
+ checkFileSizePrivilege,
20
+ getUserPlan,
21
+ getUserPlanType,
22
+ type PrivilegeCheckResult,
23
+ type UserPlanInfo,
24
+ } from "./services/user-plan";
25
+ export { subscriptionModule } from "./manifest";