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,71 @@
1
+ import { Pool as NeonPool, neonConfig } from "@neondatabase/serverless";
2
+ import { drizzle as drizzleNeonWs } from "drizzle-orm/neon-serverless";
3
+ import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
4
+ import { Pool } from "pg";
5
+
6
+ import * as schema from "./schema/index";
7
+
8
+ /**
9
+ * 数据库连接配置
10
+ *
11
+ * 支持两种模式:
12
+ * 1. Neon Serverless WebSocket (生产/测试环境) - 支持事务,兼容 Node.js 和 Edge Runtime
13
+ * 2. 标准 PostgreSQL (本地开发/Docker) - 使用连接池
14
+ *
15
+ * 注意: Neon 始终使用 WebSocket 模式以支持事务
16
+ * - Node.js 环境: 需要 ws 包提供 WebSocket
17
+ * - Edge Runtime (CF Workers/Vercel Edge): 使用原生 WebSocket API
18
+ */
19
+
20
+ // 确保环境变量存在
21
+ if (!process.env.DATABASE_URL) {
22
+ throw new Error(
23
+ "DATABASE_URL 环境变量未设置,请在 .env 文件中配置数据库连接"
24
+ );
25
+ }
26
+
27
+ const databaseUrl = process.env.DATABASE_URL;
28
+
29
+ /**
30
+ * 检测是否使用 Neon Serverless
31
+ */
32
+ const isNeon = databaseUrl.includes("neon.tech");
33
+
34
+ /**
35
+ * 检测是否在 Node.js 环境
36
+ * Edge Runtime (CF Workers, Vercel Edge) 没有 process.versions.node
37
+ */
38
+ const isNodeJs = typeof process !== "undefined" && process.versions?.node;
39
+
40
+ /**
41
+ * 创建数据库实例
42
+ * - Neon: 使用 WebSocket 连接 (支持事务,兼容 Node.js 和 Edge)
43
+ * - 标准 PG: 使用连接池 (本地开发/Docker)
44
+ */
45
+ function createDatabaseConnection() {
46
+ if (isNeon) {
47
+ // Node.js 环境需要手动设置 WebSocket 构造函数
48
+ // Edge Runtime (CF Workers, Vercel Edge) 有原生 WebSocket,无需设置
49
+ if (isNodeJs) {
50
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
51
+ const ws = require("ws");
52
+ neonConfig.webSocketConstructor = ws;
53
+ }
54
+
55
+ // 使用 WebSocket 连接池,支持事务
56
+ const pool = new NeonPool({ connectionString: databaseUrl });
57
+ return drizzleNeonWs(pool, { schema });
58
+ }
59
+
60
+ // 标准 PostgreSQL 连接池 (本地开发/Docker)
61
+ const pool = new Pool({
62
+ connectionString: databaseUrl,
63
+ });
64
+ return drizzlePg(pool, { schema });
65
+ }
66
+
67
+ // 导出数据库实例
68
+ export const db = createDatabaseConnection();
69
+
70
+ // 导出 Schema 以便在其他地方使用
71
+ export * from "./schema/index";
@@ -0,0 +1,67 @@
1
+ import { boolean, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+
3
+ export const userRoleEnum = pgEnum("user_role", ["user", "admin"]);
4
+
5
+ export const user = pgTable("user", {
6
+ id: text("id").primaryKey(),
7
+ name: text("name").notNull(),
8
+ email: text("email").notNull().unique(),
9
+ emailVerified: boolean("email_verified").notNull().default(false),
10
+ image: text("image"),
11
+ role: userRoleEnum("role").notNull().default("user"),
12
+ banned: boolean("banned").notNull().default(false),
13
+ bannedReason: text("banned_reason"),
14
+ customerId: text("customer_id").unique(),
15
+ createdAt: timestamp("created_at").notNull().defaultNow(),
16
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
17
+ });
18
+
19
+ export const session = pgTable("session", {
20
+ id: text("id").primaryKey(),
21
+ expiresAt: timestamp("expires_at").notNull(),
22
+ token: text("token").notNull().unique(),
23
+ createdAt: timestamp("created_at").notNull().defaultNow(),
24
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
25
+ ipAddress: text("ip_address"),
26
+ userAgent: text("user_agent"),
27
+ userId: text("user_id")
28
+ .notNull()
29
+ .references(() => user.id, { onDelete: "cascade" }),
30
+ });
31
+
32
+ export const account = pgTable("account", {
33
+ id: text("id").primaryKey(),
34
+ accountId: text("account_id").notNull(),
35
+ providerId: text("provider_id").notNull(),
36
+ userId: text("user_id")
37
+ .notNull()
38
+ .references(() => user.id, { onDelete: "cascade" }),
39
+ accessToken: text("access_token"),
40
+ refreshToken: text("refresh_token"),
41
+ idToken: text("id_token"),
42
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
43
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
44
+ scope: text("scope"),
45
+ password: text("password"),
46
+ createdAt: timestamp("created_at").notNull().defaultNow(),
47
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
48
+ });
49
+
50
+ export const verification = pgTable("verification", {
51
+ id: text("id").primaryKey(),
52
+ identifier: text("identifier").notNull(),
53
+ value: text("value").notNull(),
54
+ expiresAt: timestamp("expires_at").notNull(),
55
+ createdAt: timestamp("created_at").notNull().defaultNow(),
56
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
57
+ });
58
+
59
+ export type User = typeof user.$inferSelect;
60
+ export type NewUser = typeof user.$inferInsert;
61
+ export type UserRole = (typeof userRoleEnum.enumValues)[number];
62
+ export type Session = typeof session.$inferSelect;
63
+ export type NewSession = typeof session.$inferInsert;
64
+ export type Account = typeof account.$inferSelect;
65
+ export type NewAccount = typeof account.$inferInsert;
66
+ export type Verification = typeof verification.$inferSelect;
67
+ export type NewVerification = typeof verification.$inferInsert;
@@ -0,0 +1,94 @@
1
+ import {
2
+ integer,
3
+ json,
4
+ pgEnum,
5
+ pgTable,
6
+ text,
7
+ timestamp,
8
+ } from "drizzle-orm/pg-core";
9
+
10
+ import { user } from "./auth";
11
+
12
+ export const creditsBalanceStatusEnum = pgEnum("credits_balance_status", [
13
+ "active",
14
+ "frozen",
15
+ ]);
16
+ export const creditsBatchStatusEnum = pgEnum("credits_batch_status", [
17
+ "active",
18
+ "consumed",
19
+ "expired",
20
+ ]);
21
+ export const creditsBatchSourceEnum = pgEnum("credits_batch_source", [
22
+ "purchase",
23
+ "subscription",
24
+ "bonus",
25
+ "refund",
26
+ ]);
27
+ export const creditsTransactionTypeEnum = pgEnum("credits_transaction_type", [
28
+ "purchase",
29
+ "consumption",
30
+ "monthly_grant",
31
+ "registration_bonus",
32
+ "admin_grant",
33
+ "expiration",
34
+ "refund",
35
+ ]);
36
+
37
+ export const creditsBalance = pgTable("credits_balance", {
38
+ id: text("id").primaryKey(),
39
+ userId: text("user_id")
40
+ .notNull()
41
+ .unique()
42
+ .references(() => user.id, { onDelete: "cascade" }),
43
+ balance: integer("balance").notNull().default(0),
44
+ totalEarned: integer("total_earned").notNull().default(0),
45
+ totalSpent: integer("total_spent").notNull().default(0),
46
+ status: creditsBalanceStatusEnum("status").notNull().default("active"),
47
+ createdAt: timestamp("created_at").notNull().defaultNow(),
48
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
49
+ });
50
+
51
+ export const creditsBatch = pgTable("credits_batch", {
52
+ id: text("id").primaryKey(),
53
+ userId: text("user_id")
54
+ .notNull()
55
+ .references(() => user.id, { onDelete: "cascade" }),
56
+ amount: integer("amount").notNull(),
57
+ remaining: integer("remaining").notNull(),
58
+ issuedAt: timestamp("issued_at").notNull().defaultNow(),
59
+ expiresAt: timestamp("expires_at"),
60
+ status: creditsBatchStatusEnum("status").notNull().default("active"),
61
+ sourceType: creditsBatchSourceEnum("source_type").notNull(),
62
+ sourceRef: text("source_ref"),
63
+ createdAt: timestamp("created_at").notNull().defaultNow(),
64
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
65
+ });
66
+
67
+ export const creditsTransaction = pgTable("credits_transaction", {
68
+ id: text("id").primaryKey(),
69
+ userId: text("user_id")
70
+ .notNull()
71
+ .references(() => user.id, { onDelete: "cascade" }),
72
+ type: creditsTransactionTypeEnum("type").notNull(),
73
+ amount: integer("amount").notNull(),
74
+ debitAccount: text("debit_account").notNull(),
75
+ creditAccount: text("credit_account").notNull(),
76
+ description: text("description"),
77
+ metadata: json("metadata").$type<Record<string, unknown>>(),
78
+ createdAt: timestamp("created_at").notNull().defaultNow(),
79
+ });
80
+
81
+ export type CreditsBalance = typeof creditsBalance.$inferSelect;
82
+ export type NewCreditsBalance = typeof creditsBalance.$inferInsert;
83
+ export type CreditsBatch = typeof creditsBatch.$inferSelect;
84
+ export type NewCreditsBatch = typeof creditsBatch.$inferInsert;
85
+ export type CreditsTransaction = typeof creditsTransaction.$inferSelect;
86
+ export type NewCreditsTransaction = typeof creditsTransaction.$inferInsert;
87
+ export type CreditsBalanceStatus =
88
+ (typeof creditsBalanceStatusEnum.enumValues)[number];
89
+ export type CreditsBatchStatus =
90
+ (typeof creditsBatchStatusEnum.enumValues)[number];
91
+ export type CreditsBatchSource =
92
+ (typeof creditsBatchSourceEnum.enumValues)[number];
93
+ export type CreditsTransactionType =
94
+ (typeof creditsTransactionTypeEnum.enumValues)[number];
@@ -0,0 +1,5 @@
1
+ export * from "./auth";
2
+ export * from "./credits";
3
+ export * from "./mail";
4
+ export * from "./subscription";
5
+ export * from "./support";
@@ -0,0 +1,14 @@
1
+ import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+
3
+ export const newsletterSubscriber = pgTable("newsletter_subscriber", {
4
+ id: text("id").primaryKey(),
5
+ email: text("email").notNull().unique(),
6
+ isSubscribed: boolean("is_subscribed").notNull().default(true),
7
+ subscribedAt: timestamp("subscribed_at").notNull().defaultNow(),
8
+ unsubscribedAt: timestamp("unsubscribed_at"),
9
+ createdAt: timestamp("created_at").notNull().defaultNow(),
10
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
11
+ });
12
+
13
+ export type NewsletterSubscriber = typeof newsletterSubscriber.$inferSelect;
14
+ export type NewNewsletterSubscriber = typeof newsletterSubscriber.$inferInsert;
@@ -0,0 +1,21 @@
1
+ import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+
3
+ import { user } from "./auth";
4
+
5
+ export const subscription = pgTable("subscription", {
6
+ id: text("id").primaryKey(),
7
+ userId: text("user_id")
8
+ .notNull()
9
+ .references(() => user.id, { onDelete: "cascade" }),
10
+ subscriptionId: text("subscription_id").notNull().unique(),
11
+ priceId: text("price_id").notNull(),
12
+ status: text("status").notNull().default("incomplete"),
13
+ currentPeriodStart: timestamp("current_period_start"),
14
+ currentPeriodEnd: timestamp("current_period_end"),
15
+ cancelAtPeriodEnd: boolean("cancel_at_period_end").notNull().default(false),
16
+ createdAt: timestamp("created_at").notNull().defaultNow(),
17
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
18
+ });
19
+
20
+ export type Subscription = typeof subscription.$inferSelect;
21
+ export type NewSubscription = typeof subscription.$inferInsert;
@@ -0,0 +1,56 @@
1
+ import { boolean, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+
3
+ import { user } from "./auth";
4
+
5
+ export const ticketCategoryEnum = pgEnum("ticket_category", [
6
+ "billing",
7
+ "technical",
8
+ "bug",
9
+ "feature",
10
+ "other",
11
+ ]);
12
+ export const ticketPriorityEnum = pgEnum("ticket_priority", [
13
+ "low",
14
+ "medium",
15
+ "high",
16
+ ]);
17
+ export const ticketStatusEnum = pgEnum("ticket_status", [
18
+ "open",
19
+ "in_progress",
20
+ "resolved",
21
+ "closed",
22
+ ]);
23
+
24
+ export const ticket = pgTable("ticket", {
25
+ id: text("id").primaryKey(),
26
+ userId: text("user_id")
27
+ .notNull()
28
+ .references(() => user.id, { onDelete: "cascade" }),
29
+ subject: text("subject").notNull(),
30
+ category: ticketCategoryEnum("category").notNull().default("other"),
31
+ priority: ticketPriorityEnum("priority").notNull().default("medium"),
32
+ status: ticketStatusEnum("status").notNull().default("open"),
33
+ createdAt: timestamp("created_at").notNull().defaultNow(),
34
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
35
+ });
36
+
37
+ export const ticketMessage = pgTable("ticket_message", {
38
+ id: text("id").primaryKey(),
39
+ ticketId: text("ticket_id")
40
+ .notNull()
41
+ .references(() => ticket.id, { onDelete: "cascade" }),
42
+ userId: text("user_id")
43
+ .notNull()
44
+ .references(() => user.id, { onDelete: "cascade" }),
45
+ content: text("content").notNull(),
46
+ isAdminResponse: boolean("is_admin_response").notNull().default(false),
47
+ createdAt: timestamp("created_at").notNull().defaultNow(),
48
+ });
49
+
50
+ export type Ticket = typeof ticket.$inferSelect;
51
+ export type NewTicket = typeof ticket.$inferInsert;
52
+ export type TicketMessage = typeof ticketMessage.$inferSelect;
53
+ export type NewTicketMessage = typeof ticketMessage.$inferInsert;
54
+ export type TicketCategory = (typeof ticketCategoryEnum.enumValues)[number];
55
+ export type TicketPriority = (typeof ticketPriorityEnum.enumValues)[number];
56
+ export type TicketStatus = (typeof ticketStatusEnum.enumValues)[number];
@@ -0,0 +1,2 @@
1
+ // 兼容旧导入;新代码应从具体 Schema 分组或 schema/index 导入。
2
+ export * from "./schema/index";
@@ -0,0 +1,213 @@
1
+ "use client";
2
+
3
+ import { ArrowLeft, ChevronsUpDown, LogOut } from "lucide-react";
4
+ import Link from "next/link";
5
+ import { usePathname, useRouter } from "next/navigation";
6
+ import { useTranslations } from "next-intl";
7
+ import { useState } from "react";
8
+
9
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from "@/components/ui/popover";
15
+ import { Separator } from "@/components/ui/separator";
16
+ import { adminConfig, siteConfig } from "@/config";
17
+ import { ModeToggle } from "@/features/shared";
18
+ import { signOut, useSession } from "@/lib/auth/client";
19
+ import { cn } from "@/lib/utils";
20
+
21
+ /**
22
+ * Admin 侧边栏组件
23
+ *
24
+ * 根节点挂 `dark` class,让所有 token 类强制解析为暗色值——侧栏永远是
25
+ * 深色"控制台"调性,与跟随主题的主内容区形成区分(管理后台的视觉信号)。
26
+ * 红色(destructive)用于 ADMIN 标识,呼应"管理 = 需谨慎"的语义。
27
+ */
28
+ export function AdminSidebar() {
29
+ const pathname = usePathname();
30
+ const router = useRouter();
31
+ const t = useTranslations("AdminSidebar");
32
+
33
+ const getNavTitle = (title: string, translationKey?: string) =>
34
+ translationKey ? t(translationKey) : title;
35
+
36
+ const { data: session } = useSession();
37
+ const user = session?.user;
38
+ const [open, setOpen] = useState(false);
39
+
40
+ const getInitials = (name: string) =>
41
+ name
42
+ .split(" ")
43
+ .map((n) => n[0])
44
+ .join("")
45
+ .toUpperCase()
46
+ .slice(0, 2);
47
+
48
+ const handleSignOut = async () => {
49
+ setOpen(false);
50
+ await signOut({
51
+ fetchOptions: {
52
+ onSuccess: () => {
53
+ router.push("/");
54
+ },
55
+ },
56
+ });
57
+ };
58
+
59
+ return (
60
+ <aside className="dark fixed left-0 top-0 z-40 flex h-screen w-64 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground">
61
+ {/* Logo - Admin 标识 */}
62
+ <div className="flex h-14 items-center gap-2 border-b border-sidebar-border px-4">
63
+ {/* biome-ignore lint/a11y/noSvgWithoutTitle: decorative logo */}
64
+ <svg
65
+ className="h-6 w-6 shrink-0 text-primary"
66
+ viewBox="0 0 24 24"
67
+ fill="none"
68
+ stroke="currentColor"
69
+ strokeWidth={2.4}
70
+ strokeLinecap="round"
71
+ strokeLinejoin="round"
72
+ >
73
+ <path d="M3 17 L9 7 L13 14 L21 4" />
74
+ <circle cx="9" cy="7" r="1.4" fill="currentColor" stroke="none" />
75
+ <circle cx="21" cy="4" r="1.4" fill="currentColor" stroke="none" />
76
+ </svg>
77
+ <span className="text-base font-bold tracking-tight">
78
+ {siteConfig.name}
79
+ </span>
80
+ <span className="rounded bg-destructive px-1.5 py-0.5 font-mono text-[10px] font-semibold tracking-[0.12em] text-white">
81
+ {t("badge")}
82
+ </span>
83
+ </div>
84
+
85
+ {/* 导航菜单 */}
86
+ <nav className="flex-1 space-y-6 overflow-y-auto p-3">
87
+ {adminConfig.sidebarNav.map((group) => (
88
+ <div key={group.title}>
89
+ <p className="mb-1.5 px-2 font-mono text-[10px] font-semibold tracking-[0.14em] text-sidebar-foreground/40 uppercase">
90
+ {getNavTitle(group.title, group.translationKey)}
91
+ </p>
92
+ <div className="space-y-0.5">
93
+ {group.items.map((item) => {
94
+ const isActive = pathname.endsWith(item.href);
95
+ const Icon = item.icon;
96
+ return (
97
+ <Link
98
+ key={item.href}
99
+ href={item.href}
100
+ className={cn(
101
+ "flex items-center gap-3 rounded-md px-2 py-1.5 text-sm font-medium transition-colors",
102
+ isActive
103
+ ? "bg-primary/10 text-primary"
104
+ : "text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
105
+ )}
106
+ >
107
+ {Icon && <Icon className="h-4 w-4 shrink-0" />}
108
+ {getNavTitle(item.title, item.translationKey)}
109
+ </Link>
110
+ );
111
+ })}
112
+ </div>
113
+ </div>
114
+ ))}
115
+
116
+ {/* 返回用户端链接 */}
117
+ <div className="border-t border-sidebar-border pt-3">
118
+ <Link
119
+ href="/dashboard"
120
+ className="flex items-center gap-3 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
121
+ >
122
+ <ArrowLeft className="h-4 w-4" />
123
+ {t("returnToUser")}
124
+ </Link>
125
+ </div>
126
+ </nav>
127
+
128
+ {/* 用户信息区域 */}
129
+ <div className="border-t border-sidebar-border p-3">
130
+ {user ? (
131
+ <Popover open={open} onOpenChange={setOpen}>
132
+ <PopoverTrigger asChild>
133
+ <button
134
+ type="button"
135
+ className="flex w-full items-center gap-3 rounded-md px-2 py-1.5 transition-colors hover:bg-sidebar-accent/50"
136
+ >
137
+ <Avatar className="h-8 w-8 shrink-0">
138
+ <AvatarImage src={user.image || undefined} alt={user.name} />
139
+ <AvatarFallback className="bg-destructive text-xs text-white">
140
+ {getInitials(user.name)}
141
+ </AvatarFallback>
142
+ </Avatar>
143
+ <div className="flex-1 truncate text-left">
144
+ <div className="flex items-center gap-2">
145
+ <p className="text-sm font-medium">{user.name}</p>
146
+ <span className="rounded bg-destructive/15 px-1.5 py-0.5 font-mono text-[10px] font-semibold text-destructive">
147
+ {t("roleBadge")}
148
+ </span>
149
+ </div>
150
+ <p className="truncate text-xs text-sidebar-foreground/50">
151
+ {user.email}
152
+ </p>
153
+ </div>
154
+ <ChevronsUpDown className="h-4 w-4 text-sidebar-foreground/50" />
155
+ </button>
156
+ </PopoverTrigger>
157
+
158
+ <PopoverContent
159
+ side="top"
160
+ align="start"
161
+ sideOffset={8}
162
+ className="w-64 p-0"
163
+ >
164
+ <div className="flex items-center gap-3 p-4">
165
+ <Avatar className="h-10 w-10">
166
+ <AvatarImage src={user.image || undefined} alt={user.name} />
167
+ <AvatarFallback className="bg-destructive text-white">
168
+ {getInitials(user.name)}
169
+ </AvatarFallback>
170
+ </Avatar>
171
+ <div className="flex-1 truncate">
172
+ <p className="font-medium">{user.name}</p>
173
+ <p className="truncate text-sm text-muted-foreground">
174
+ {user.email}
175
+ </p>
176
+ </div>
177
+ </div>
178
+
179
+ <Separator />
180
+
181
+ {/* 主题切换 - 用共享 ModeToggle,真正生效 */}
182
+ <div className="flex items-center justify-center p-3">
183
+ <ModeToggle variant="inline" />
184
+ </div>
185
+
186
+ <Separator />
187
+
188
+ <div className="p-2">
189
+ <button
190
+ type="button"
191
+ onClick={handleSignOut}
192
+ className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-muted"
193
+ >
194
+ <LogOut className="h-4 w-4" />
195
+ {t("signOut")}
196
+ </button>
197
+ </div>
198
+ </PopoverContent>
199
+ </Popover>
200
+ ) : (
201
+ // 加载状态
202
+ <div className="flex items-center gap-3 rounded-md px-2 py-1.5">
203
+ <div className="h-8 w-8 animate-pulse rounded-full bg-sidebar-accent" />
204
+ <div className="flex-1 space-y-1">
205
+ <div className="h-4 w-20 animate-pulse rounded bg-sidebar-accent" />
206
+ <div className="h-3 w-32 animate-pulse rounded bg-sidebar-accent" />
207
+ </div>
208
+ </div>
209
+ )}
210
+ </div>
211
+ </aside>
212
+ );
213
+ }
@@ -0,0 +1,2 @@
1
+ // Admin 组件导出
2
+ export { AdminSidebar } from "./admin-sidebar";
@@ -0,0 +1,3 @@
1
+ // Admin Feature
2
+ export * from "./components";
3
+ export { adminModule } from "./manifest";
@@ -0,0 +1,41 @@
1
+ import { defineModule } from "@/core/modules";
2
+
3
+ export const adminModule = defineModule({
4
+ id: "admin",
5
+ name: "Admin",
6
+ description: "管理后台、用户管理与平台运营入口",
7
+ kind: "optional",
8
+ dependencies: ["shared", "support"],
9
+ routes: [
10
+ {
11
+ path: "/admin",
12
+ source: "src/app/[locale]/(admin)/admin/page.tsx",
13
+ },
14
+ {
15
+ path: "/admin/users",
16
+ source: "src/app/[locale]/(admin)/admin/users/page.tsx",
17
+ },
18
+ ],
19
+ navigation: [
20
+ {
21
+ area: "admin",
22
+ group: "control-panel",
23
+ groupTranslationKey: "nav.controlPanel",
24
+ translationKey: "nav.dashboard",
25
+ href: "/admin",
26
+ icon: "dashboard",
27
+ order: 10,
28
+ },
29
+ {
30
+ area: "admin",
31
+ group: "control-panel",
32
+ groupTranslationKey: "nav.controlPanel",
33
+ translationKey: "nav.userManagement",
34
+ href: "/admin/users",
35
+ icon: "users",
36
+ order: 20,
37
+ },
38
+ ],
39
+ translations: ["AdminSidebar"],
40
+ schema: [],
41
+ });
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { GoogleAnalytics } from "@next/third-parties/google";
4
+ import { useEffect, useState } from "react";
5
+
6
+ import {
7
+ COOKIE_CONSENT_CHANGE_EVENT,
8
+ COOKIE_CONSENT_KEY,
9
+ } from "@/lib/cookie-consent";
10
+
11
+ /**
12
+ * Analytics 组件
13
+ *
14
+ * 功能:
15
+ * - 条件渲染 Google Analytics
16
+ * - 仅在用户接受 Cookie 时加载
17
+ * - 监听 localStorage 变化以响应用户偏好更改
18
+ */
19
+ export function Analytics() {
20
+ const [hasConsent, setHasConsent] = useState(false);
21
+ const gaId = process.env.NEXT_PUBLIC_GA_ID;
22
+
23
+ useEffect(() => {
24
+ // 检查初始同意状态
25
+ const checkConsent = () => {
26
+ const consent = localStorage.getItem(COOKIE_CONSENT_KEY);
27
+ setHasConsent(consent === "all");
28
+ };
29
+
30
+ checkConsent();
31
+
32
+ // 监听 storage 事件以响应其他标签页的更改
33
+ const handleStorageChange = (e: StorageEvent) => {
34
+ if (e.key === COOKIE_CONSENT_KEY) {
35
+ checkConsent();
36
+ }
37
+ };
38
+
39
+ // 监听自定义事件以响应同一页面的更改
40
+ const handleConsentChange = () => {
41
+ checkConsent();
42
+ };
43
+
44
+ window.addEventListener("storage", handleStorageChange);
45
+ window.addEventListener(COOKIE_CONSENT_CHANGE_EVENT, handleConsentChange);
46
+
47
+ return () => {
48
+ window.removeEventListener("storage", handleStorageChange);
49
+ window.removeEventListener(
50
+ COOKIE_CONSENT_CHANGE_EVENT,
51
+ handleConsentChange
52
+ );
53
+ };
54
+ }, []);
55
+
56
+ // 未配置 GA ID 或未同意时不渲染
57
+ if (!gaId || !hasConsent) {
58
+ return null;
59
+ }
60
+
61
+ return <GoogleAnalytics gaId={gaId} />;
62
+ }
@@ -0,0 +1,2 @@
1
+ // Analytics Components
2
+ export { Analytics } from "./analytics";
@@ -0,0 +1,3 @@
1
+ // Analytics System
2
+ export * from "./components";
3
+ export { analyticsModule } from "./manifest";