create-crm-starter 0.1.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 (261) hide show
  1. package/README.md +53 -0
  2. package/dist/index.js +2341 -0
  3. package/package.json +51 -0
  4. package/template/base/_dot_gitignore +11 -0
  5. package/template/base/eslint.config.mjs +7 -0
  6. package/template/base/next.config.ts +17 -0
  7. package/template/base/package.json +35 -0
  8. package/template/base/postcss.config.mjs +7 -0
  9. package/template/base/scripts/setup.mjs +146 -0
  10. package/template/base/src/app/globals.css +68 -0
  11. package/template/base/src/app/layout.tsx +19 -0
  12. package/template/base/src/app/page.tsx +37 -0
  13. package/template/base/src/components/ui/badge.tsx +29 -0
  14. package/template/base/src/components/ui/button.tsx +49 -0
  15. package/template/base/src/components/ui/card.tsx +54 -0
  16. package/template/base/src/components/ui/input.tsx +19 -0
  17. package/template/base/src/components/ui/label.tsx +19 -0
  18. package/template/base/src/components/ui/separator.tsx +25 -0
  19. package/template/base/src/components/ui/skeleton.tsx +7 -0
  20. package/template/base/src/lib/utils.ts +17 -0
  21. package/template/base/tsconfig.json +21 -0
  22. package/template/extras/auth-better-auth/src/app/(auth)/layout.tsx +7 -0
  23. package/template/extras/auth-better-auth/src/app/(auth)/sign-in/page.tsx +65 -0
  24. package/template/extras/auth-better-auth/src/app/(auth)/sign-up/page.tsx +70 -0
  25. package/template/extras/auth-better-auth/src/app/(dashboard)/admin/page.tsx +68 -0
  26. package/template/extras/auth-better-auth/src/app/(dashboard)/dashboard/page.tsx +42 -0
  27. package/template/extras/auth-better-auth/src/app/(dashboard)/layout.tsx +41 -0
  28. package/template/extras/auth-better-auth/src/app/api/auth/[...all]/route.ts +4 -0
  29. package/template/extras/auth-better-auth/src/components/sign-out-button.tsx +19 -0
  30. package/template/extras/auth-better-auth/src/lib/auth-client.ts +7 -0
  31. package/template/extras/auth-better-auth/src/lib/auth-server.ts +51 -0
  32. package/template/extras/auth-better-auth/src/lib/auth.ts +76 -0
  33. package/template/extras/auth-better-auth/src/middleware.ts +25 -0
  34. package/template/extras/auth-clerk/src/app/(auth)/layout.tsx +7 -0
  35. package/template/extras/auth-clerk/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +5 -0
  36. package/template/extras/auth-clerk/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +5 -0
  37. package/template/extras/auth-clerk/src/app/(dashboard)/admin/page.tsx +37 -0
  38. package/template/extras/auth-clerk/src/app/(dashboard)/dashboard/page.tsx +42 -0
  39. package/template/extras/auth-clerk/src/app/(dashboard)/layout.tsx +57 -0
  40. package/template/extras/auth-clerk/src/lib/auth.ts +55 -0
  41. package/template/extras/auth-clerk/src/middleware.ts +17 -0
  42. package/template/extras/calendar-dispatch/_shared/src/app/(dashboard)/calendar/page.tsx +39 -0
  43. package/template/extras/calendar-dispatch/drizzle/src/app/(dashboard)/calendar/page.tsx +21 -0
  44. package/template/extras/calendar-dispatch/drizzle/src/components/calendar/calendar-board.tsx +195 -0
  45. package/template/extras/calendar-dispatch/drizzle/src/lib/calendar/actions.ts +35 -0
  46. package/template/extras/calendar-dispatch/drizzle/src/lib/calendar/data.ts +74 -0
  47. package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/[id]/page.tsx +48 -0
  48. package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/new/page.tsx +15 -0
  49. package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/page.tsx +83 -0
  50. package/template/extras/checklists/_shared/src/components/jobs/job-checklists-section.tsx +18 -0
  51. package/template/extras/checklists/_shared/src/lib/checklists/data.ts +17 -0
  52. package/template/extras/checklists/_shared/src/lib/checklists/sample-data.ts +56 -0
  53. package/template/extras/checklists/_shared/src/lib/checklists/types.ts +47 -0
  54. package/template/extras/checklists/drizzle/src/app/(dashboard)/checklists/new/page.tsx +10 -0
  55. package/template/extras/checklists/drizzle/src/components/checklists/new-template-form.tsx +158 -0
  56. package/template/extras/checklists/drizzle/src/components/jobs/job-checklists-client.tsx +202 -0
  57. package/template/extras/checklists/drizzle/src/components/jobs/job-checklists-section.tsx +24 -0
  58. package/template/extras/checklists/drizzle/src/db/schema/checklists.ts +52 -0
  59. package/template/extras/checklists/drizzle/src/lib/checklists/actions.ts +112 -0
  60. package/template/extras/checklists/drizzle/src/lib/checklists/data.ts +80 -0
  61. package/template/extras/comms-email/src/app/(dashboard)/email/page.tsx +32 -0
  62. package/template/extras/comms-sms/_shared/src/app/(dashboard)/sms/new/page.tsx +35 -0
  63. package/template/extras/comms-sms/_shared/src/app/(dashboard)/sms/page.tsx +55 -0
  64. package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/[customerId]/page.tsx +102 -0
  65. package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/new/page.tsx +120 -0
  66. package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/page.tsx +70 -0
  67. package/template/extras/comms-sms/drizzle/src/app/api/twilio/sms/route.ts +69 -0
  68. package/template/extras/comms-sms/drizzle/src/db/schema/sms-messages.ts +27 -0
  69. package/template/extras/comms-sms/drizzle/src/lib/sms/actions.ts +107 -0
  70. package/template/extras/comms-sms/drizzle/src/lib/sms/data.ts +111 -0
  71. package/template/extras/customer-portal/_shared/src/app/portal/[token]/layout.tsx +51 -0
  72. package/template/extras/customer-portal/drizzle/src/app/portal/[token]/appointments/page.tsx +78 -0
  73. package/template/extras/customer-portal/drizzle/src/app/portal/[token]/estimates/page.tsx +94 -0
  74. package/template/extras/customer-portal/drizzle/src/app/portal/[token]/invoices/page.tsx +71 -0
  75. package/template/extras/customer-portal/drizzle/src/app/portal/[token]/page.tsx +118 -0
  76. package/template/extras/customer-portal/drizzle/src/components/portal/estimate-approval.tsx +141 -0
  77. package/template/extras/customer-portal/drizzle/src/components/portal/signature-pad.tsx +126 -0
  78. package/template/extras/customer-portal/drizzle/src/lib/portal/actions.ts +107 -0
  79. package/template/extras/customer-portal/drizzle/src/lib/portal/data.ts +158 -0
  80. package/template/extras/customers/_fragments/convex.txt +28 -0
  81. package/template/extras/customers/_shared/src/app/(dashboard)/customers/[id]/page.tsx +81 -0
  82. package/template/extras/customers/_shared/src/app/(dashboard)/customers/new/page.tsx +16 -0
  83. package/template/extras/customers/_shared/src/app/(dashboard)/customers/page.tsx +73 -0
  84. package/template/extras/customers/_shared/src/lib/customers/data.ts +15 -0
  85. package/template/extras/customers/_shared/src/lib/customers/sample-data.ts +67 -0
  86. package/template/extras/customers/_shared/src/lib/customers/types.ts +31 -0
  87. package/template/extras/customers/convex/convex/customers.ts +52 -0
  88. package/template/extras/customers/convex/src/lib/customers/data.ts +64 -0
  89. package/template/extras/customers/drizzle/src/app/(dashboard)/customers/new/page.tsx +82 -0
  90. package/template/extras/customers/drizzle/src/db/schema/customers.ts +34 -0
  91. package/template/extras/customers/drizzle/src/lib/customers/actions.ts +67 -0
  92. package/template/extras/customers/drizzle/src/lib/customers/data.ts +34 -0
  93. package/template/extras/db-convex/convex/_generated/README.md +13 -0
  94. package/template/extras/db-convex/convex/_generated/api.d.ts +8 -0
  95. package/template/extras/db-convex/convex/_generated/api.js +12 -0
  96. package/template/extras/db-convex/convex/_generated/dataModel.d.ts +9 -0
  97. package/template/extras/db-convex/convex/_generated/server.d.ts +18 -0
  98. package/template/extras/db-convex/convex/_generated/server.js +12 -0
  99. package/template/extras/db-convex/convex/auth.config.ts +17 -0
  100. package/template/extras/db-convex/convex/schema.ts +28 -0
  101. package/template/extras/db-convex/src/app/layout.tsx +20 -0
  102. package/template/extras/db-convex/src/components/providers.tsx +28 -0
  103. package/template/extras/db-convex/src/lib/convex.ts +6 -0
  104. package/template/extras/db-drizzle-pg/docker-compose.yml +21 -0
  105. package/template/extras/db-drizzle-pg/drizzle.config.ts +24 -0
  106. package/template/extras/db-drizzle-pg/src/app/layout.tsx +20 -0
  107. package/template/extras/db-drizzle-pg/src/components/providers.tsx +16 -0
  108. package/template/extras/db-drizzle-pg/src/db/client.ts +14 -0
  109. package/template/extras/db-drizzle-pg/src/db/schema/auth.ts +62 -0
  110. package/template/extras/db-drizzle-pg/src/db/schema/index.ts +3 -0
  111. package/template/extras/emergency-dispatch/src/app/(dashboard)/emergency/page.tsx +43 -0
  112. package/template/extras/equipment-tracking/src/app/(dashboard)/equipment/page.tsx +61 -0
  113. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/[id]/page.tsx +123 -0
  114. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/new/page.tsx +22 -0
  115. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/page.tsx +102 -0
  116. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/invoices/[id]/page.tsx +168 -0
  117. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/invoices/page.tsx +100 -0
  118. package/template/extras/estimates-invoices/_shared/src/components/estimates/convert-to-job-button.tsx +14 -0
  119. package/template/extras/estimates-invoices/_shared/src/components/invoices/pay-invoice-button.tsx +15 -0
  120. package/template/extras/estimates-invoices/_shared/src/components/invoices/send-invoice-email-button.tsx +14 -0
  121. package/template/extras/estimates-invoices/_shared/src/lib/estimates/data.ts +14 -0
  122. package/template/extras/estimates-invoices/_shared/src/lib/estimates/sample-data.ts +74 -0
  123. package/template/extras/estimates-invoices/_shared/src/lib/estimates/types.ts +60 -0
  124. package/template/extras/estimates-invoices/_shared/src/lib/invoices/data.ts +14 -0
  125. package/template/extras/estimates-invoices/_shared/src/lib/invoices/sample-data.ts +83 -0
  126. package/template/extras/estimates-invoices/_shared/src/lib/invoices/types.ts +78 -0
  127. package/template/extras/estimates-invoices/drizzle/src/app/(dashboard)/estimates/new/page.tsx +18 -0
  128. package/template/extras/estimates-invoices/drizzle/src/app/api/stripe/webhook/route.ts +87 -0
  129. package/template/extras/estimates-invoices/drizzle/src/app/i/[token]/page.tsx +148 -0
  130. package/template/extras/estimates-invoices/drizzle/src/components/estimates/convert-to-job-button.tsx +18 -0
  131. package/template/extras/estimates-invoices/drizzle/src/components/estimates/new-estimate-form.tsx +261 -0
  132. package/template/extras/estimates-invoices/drizzle/src/components/invoices/pay-invoice-button.tsx +19 -0
  133. package/template/extras/estimates-invoices/drizzle/src/components/invoices/public-pay-button.tsx +20 -0
  134. package/template/extras/estimates-invoices/drizzle/src/components/invoices/send-invoice-email-button.tsx +37 -0
  135. package/template/extras/estimates-invoices/drizzle/src/components/jobs/generate-invoice-button.tsx +23 -0
  136. package/template/extras/estimates-invoices/drizzle/src/db/schema/estimates.ts +41 -0
  137. package/template/extras/estimates-invoices/drizzle/src/db/schema/invoices.ts +59 -0
  138. package/template/extras/estimates-invoices/drizzle/src/lib/estimates/actions.ts +110 -0
  139. package/template/extras/estimates-invoices/drizzle/src/lib/estimates/data.ts +57 -0
  140. package/template/extras/estimates-invoices/drizzle/src/lib/invoices/actions.ts +199 -0
  141. package/template/extras/estimates-invoices/drizzle/src/lib/invoices/data.ts +99 -0
  142. package/template/extras/estimates-invoices/drizzle/src/lib/invoices/email-actions.ts +102 -0
  143. package/template/extras/inspection-checklists/src/app/(dashboard)/inspections/page.tsx +60 -0
  144. package/template/extras/jobs/_fragments/convex.txt +21 -0
  145. package/template/extras/jobs/_shared/src/app/(dashboard)/jobs/[id]/page.tsx +102 -0
  146. package/template/extras/jobs/_shared/src/app/(dashboard)/jobs/page.tsx +72 -0
  147. package/template/extras/jobs/_shared/src/components/jobs/advance-status-button.tsx +21 -0
  148. package/template/extras/jobs/_shared/src/components/jobs/generate-invoice-button.tsx +15 -0
  149. package/template/extras/jobs/_shared/src/components/jobs/photo-gallery.tsx +17 -0
  150. package/template/extras/jobs/_shared/src/components/jobs/photos-section.tsx +18 -0
  151. package/template/extras/jobs/_shared/src/lib/jobs/data.ts +14 -0
  152. package/template/extras/jobs/_shared/src/lib/jobs/sample-data.ts +50 -0
  153. package/template/extras/jobs/_shared/src/lib/jobs/types.ts +62 -0
  154. package/template/extras/jobs/convex/convex/jobs.ts +46 -0
  155. package/template/extras/jobs/convex/src/lib/jobs/data.ts +65 -0
  156. package/template/extras/jobs/drizzle/src/app/(dashboard)/jobs/new/page.tsx +18 -0
  157. package/template/extras/jobs/drizzle/src/components/jobs/advance-status-button.tsx +34 -0
  158. package/template/extras/jobs/drizzle/src/components/jobs/new-job-form.tsx +275 -0
  159. package/template/extras/jobs/drizzle/src/components/jobs/photo-gallery.tsx +130 -0
  160. package/template/extras/jobs/drizzle/src/components/jobs/photos-section.tsx +7 -0
  161. package/template/extras/jobs/drizzle/src/db/schema/job-attachments.ts +26 -0
  162. package/template/extras/jobs/drizzle/src/db/schema/jobs.ts +29 -0
  163. package/template/extras/jobs/drizzle/src/lib/jobs/actions.ts +71 -0
  164. package/template/extras/jobs/drizzle/src/lib/jobs/data.ts +48 -0
  165. package/template/extras/jobs/drizzle/src/lib/jobs/photos-actions.ts +121 -0
  166. package/template/extras/jobs/drizzle/src/lib/r2.ts +45 -0
  167. package/template/extras/landing-page/_shared/src/app/book/page.tsx +43 -0
  168. package/template/extras/landing-page/_shared/src/app/book/thanks/page.tsx +31 -0
  169. package/template/extras/landing-page/_shared/src/app/page.tsx +81 -0
  170. package/template/extras/landing-page/drizzle/src/app/book/page.tsx +105 -0
  171. package/template/extras/landing-page/drizzle/src/lib/booking/actions.ts +97 -0
  172. package/template/extras/maintenance-plans/src/app/(dashboard)/maintenance-plans/page.tsx +72 -0
  173. package/template/extras/mobile/mobile/README.md +67 -0
  174. package/template/extras/mobile/mobile/_dot_env.example +5 -0
  175. package/template/extras/mobile/mobile/_dot_gitignore +26 -0
  176. package/template/extras/mobile/mobile/app/(app)/_layout.tsx +37 -0
  177. package/template/extras/mobile/mobile/app/(app)/estimate.tsx +135 -0
  178. package/template/extras/mobile/mobile/app/(app)/inbox/[customerId].tsx +103 -0
  179. package/template/extras/mobile/mobile/app/(app)/inbox/index.tsx +70 -0
  180. package/template/extras/mobile/mobile/app/(app)/index.tsx +111 -0
  181. package/template/extras/mobile/mobile/app/(app)/job/[id]/checklist.tsx +99 -0
  182. package/template/extras/mobile/mobile/app/(app)/job/[id]/invoice.tsx +143 -0
  183. package/template/extras/mobile/mobile/app/(app)/job/[id].tsx +259 -0
  184. package/template/extras/mobile/mobile/app/_layout.tsx +14 -0
  185. package/template/extras/mobile/mobile/app/index.tsx +23 -0
  186. package/template/extras/mobile/mobile/app/sign-in.tsx +101 -0
  187. package/template/extras/mobile/mobile/app.brand.ts +14 -0
  188. package/template/extras/mobile/mobile/app.config.ts +40 -0
  189. package/template/extras/mobile/mobile/app.features.ts +11 -0
  190. package/template/extras/mobile/mobile/components/SignaturePad.tsx +60 -0
  191. package/template/extras/mobile/mobile/eas.json +22 -0
  192. package/template/extras/mobile/mobile/lib/api.ts +253 -0
  193. package/template/extras/mobile/mobile/lib/auth.ts +51 -0
  194. package/template/extras/mobile/mobile/lib/format.ts +23 -0
  195. package/template/extras/mobile/mobile/lib/push.ts +24 -0
  196. package/template/extras/mobile/mobile/lib/theme.ts +16 -0
  197. package/template/extras/mobile/mobile/package.json +34 -0
  198. package/template/extras/mobile/mobile/tsconfig.json +11 -0
  199. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/customers/[id]/route.ts +18 -0
  200. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/devices/route.ts +40 -0
  201. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/attachments/route.ts +59 -0
  202. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/checklists/item/route.ts +34 -0
  203. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/checklists/route.ts +15 -0
  204. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/route.ts +35 -0
  205. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/status/route.ts +28 -0
  206. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/clock-in/route.ts +35 -0
  207. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/clock-out/route.ts +27 -0
  208. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/route.ts +36 -0
  209. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/route.ts +30 -0
  210. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/me/route.ts +26 -0
  211. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/uploads/sign/route.ts +46 -0
  212. package/template/extras/mobile-api/drizzle/src/db/schema/push-tokens.ts +21 -0
  213. package/template/extras/mobile-api/drizzle/src/db/schema/time-entries.ts +23 -0
  214. package/template/extras/mobile-api/drizzle/src/lib/mobile/cors.ts +30 -0
  215. package/template/extras/mobile-api/drizzle/src/lib/mobile/guard.ts +49 -0
  216. package/template/extras/mobile-api/drizzle/src/lib/push/send.ts +56 -0
  217. package/template/extras/mobile-api/estimates/src/app/api/mobile/v1/estimates/route.ts +52 -0
  218. package/template/extras/mobile-api/estimates/src/app/api/mobile/v1/price-book/route.ts +14 -0
  219. package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/invoices/[id]/payments/route.ts +32 -0
  220. package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/invoices/[id]/route.ts +28 -0
  221. package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/jobs/[id]/invoice/route.ts +82 -0
  222. package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/send/route.ts +32 -0
  223. package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/threads/[customerId]/route.ts +15 -0
  224. package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/threads/route.ts +14 -0
  225. package/template/extras/payments-stripe/src/app/(dashboard)/payments/page.tsx +56 -0
  226. package/template/extras/payments-stripe/src/app/api/stripe/webhook/route.ts +56 -0
  227. package/template/extras/payments-stripe/src/components/payments/demo-checkout-button.tsx +18 -0
  228. package/template/extras/payments-stripe/src/lib/payments/actions.ts +63 -0
  229. package/template/extras/payments-stripe/src/lib/stripe.ts +25 -0
  230. package/template/extras/permit-tracking/src/app/(dashboard)/permits/page.tsx +56 -0
  231. package/template/extras/price-book/_shared/src/app/(dashboard)/price-book/new/page.tsx +20 -0
  232. package/template/extras/price-book/_shared/src/app/(dashboard)/price-book/page.tsx +79 -0
  233. package/template/extras/price-book/_shared/src/components/price-book/item-picker.tsx +145 -0
  234. package/template/extras/price-book/_shared/src/lib/price-book/actions.ts +14 -0
  235. package/template/extras/price-book/_shared/src/lib/price-book/data.ts +28 -0
  236. package/template/extras/price-book/_shared/src/lib/price-book/sample-data.ts +62 -0
  237. package/template/extras/price-book/_shared/src/lib/price-book/types.ts +35 -0
  238. package/template/extras/price-book/drizzle/src/app/(dashboard)/price-book/new/page.tsx +18 -0
  239. package/template/extras/price-book/drizzle/src/components/price-book/new-item-form.tsx +254 -0
  240. package/template/extras/price-book/drizzle/src/db/schema/price-book.ts +33 -0
  241. package/template/extras/price-book/drizzle/src/lib/price-book/actions.ts +72 -0
  242. package/template/extras/price-book/drizzle/src/lib/price-book/data.ts +81 -0
  243. package/template/extras/reporting/src/app/(dashboard)/reports/page.tsx +66 -0
  244. package/template/extras/reviews/src/app/(dashboard)/reviews/page.tsx +58 -0
  245. package/template/extras/seed/_shared/scripts/seed.ts +35 -0
  246. package/template/extras/seed/drizzle/scripts/seed.ts +314 -0
  247. package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/[id]/page.tsx +114 -0
  248. package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/new/page.tsx +18 -0
  249. package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/page.tsx +92 -0
  250. package/template/extras/service-plans/_shared/src/components/service-plans/subscribe-customer-section.tsx +17 -0
  251. package/template/extras/service-plans/_shared/src/lib/service-plans/data.ts +14 -0
  252. package/template/extras/service-plans/_shared/src/lib/service-plans/sample-data.ts +83 -0
  253. package/template/extras/service-plans/_shared/src/lib/service-plans/types.ts +57 -0
  254. package/template/extras/service-plans/drizzle/src/app/(dashboard)/service-plans/new/page.tsx +10 -0
  255. package/template/extras/service-plans/drizzle/src/app/api/stripe/webhook/route.ts +143 -0
  256. package/template/extras/service-plans/drizzle/src/components/service-plans/new-plan-form.tsx +126 -0
  257. package/template/extras/service-plans/drizzle/src/components/service-plans/subscribe-customer-form.tsx +88 -0
  258. package/template/extras/service-plans/drizzle/src/components/service-plans/subscribe-customer-section.tsx +12 -0
  259. package/template/extras/service-plans/drizzle/src/db/schema/service-plans.ts +46 -0
  260. package/template/extras/service-plans/drizzle/src/lib/service-plans/actions.ts +196 -0
  261. package/template/extras/service-plans/drizzle/src/lib/service-plans/data.ts +124 -0
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "create-crm-starter",
3
+ "version": "0.1.0",
4
+ "description": "Interactive scaffolder for Next.js home-services CRMs (HVAC, Plumbing, Electrical, general).",
5
+ "keywords": [
6
+ "crm",
7
+ "home-services",
8
+ "hvac",
9
+ "plumbing",
10
+ "electrical",
11
+ "scaffolder",
12
+ "nextjs",
13
+ "create-app",
14
+ "starter"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Doug Allen <doug@aideveloper.dev>",
18
+ "type": "module",
19
+ "bin": {
20
+ "create-crm-starter": "./dist/index.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "template"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.18"
31
+ },
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "dev": "tsup --watch",
35
+ "scaffold": "node dist/index.js",
36
+ "typecheck": "tsc --noEmit",
37
+ "prepublishOnly": "pnpm build"
38
+ },
39
+ "dependencies": {
40
+ "@clack/prompts": "^0.8.2",
41
+ "citty": "^0.1.6",
42
+ "execa": "^9.5.2",
43
+ "package-manager-detector": "^0.2.6",
44
+ "picocolors": "^1.1.1"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.10.1",
48
+ "tsup": "^8.3.5",
49
+ "typescript": "^5.7.2"
50
+ }
51
+ }
@@ -0,0 +1,11 @@
1
+ node_modules
2
+ .next
3
+ .env
4
+ .env.local
5
+ .env*.local
6
+ *.log
7
+ .DS_Store
8
+ coverage
9
+ .turbo
10
+ .vercel
11
+ next-env.d.ts
@@ -0,0 +1,7 @@
1
+ import { FlatCompat } from '@eslint/eslintrc';
2
+
3
+ const compat = new FlatCompat({ baseDirectory: import.meta.dirname });
4
+
5
+ const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')];
6
+
7
+ export default eslintConfig;
@@ -0,0 +1,17 @@
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {
4
+ reactStrictMode: true,
5
+ eslint: {
6
+ // Lint is a dev-time concern (`pnpm lint`) — it shouldn't block a
7
+ // production deploy. TypeScript checking stays on (see below), so type
8
+ // errors still fail the build.
9
+ ignoreDuringBuilds: true,
10
+ },
11
+ typescript: {
12
+ // Keep type-safety enforced at build time. `pnpm typecheck` must pass.
13
+ ignoreBuildErrors: false,
14
+ },
15
+ };
16
+
17
+ export default nextConfig;
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "crm-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint",
11
+ "typecheck": "tsc --noEmit",
12
+ "setup": "node scripts/setup.mjs"
13
+ },
14
+ "dependencies": {
15
+ "@radix-ui/react-slot": "^1.2.4",
16
+ "class-variance-authority": "^0.7.1",
17
+ "clsx": "^2.1.1",
18
+ "lucide-react": "^0.469.0",
19
+ "next": "^15.5.14",
20
+ "react": "^19.0.0",
21
+ "react-dom": "^19.0.0",
22
+ "tailwind-merge": "^3.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@eslint/eslintrc": "^3.2.0",
26
+ "@tailwindcss/postcss": "^4.0.0",
27
+ "@types/node": "^22.10.1",
28
+ "@types/react": "^19.0.0",
29
+ "@types/react-dom": "^19.0.0",
30
+ "eslint": "^9.17.0",
31
+ "eslint-config-next": "^15.1.0",
32
+ "tailwindcss": "^4.0.0",
33
+ "typescript": "^5.7.2"
34
+ }
35
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable */
3
+ // Interactive env wizard. Walks through every required env var, writes
4
+ // answers to .env.local. Skips entries that already have non-empty values.
5
+ //
6
+ // Run with: pnpm setup
7
+
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import readline from 'node:readline';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const here = path.dirname(fileURLToPath(import.meta.url));
14
+ const projectRoot = path.resolve(here, '..');
15
+ const envLocalPath = path.join(projectRoot, '.env.local');
16
+ const envExamplePath = path.join(projectRoot, '.env.example');
17
+
18
+ const COLORS = {
19
+ reset: '\x1b[0m',
20
+ bold: '\x1b[1m',
21
+ dim: '\x1b[2m',
22
+ cyan: '\x1b[36m',
23
+ green: '\x1b[32m',
24
+ yellow: '\x1b[33m',
25
+ red: '\x1b[31m',
26
+ };
27
+ const c = (color, s) => `${COLORS[color]}${s}${COLORS.reset}`;
28
+
29
+ async function readEnvFile(filePath) {
30
+ try {
31
+ const text = await fs.readFile(filePath, 'utf8');
32
+ const map = new Map();
33
+ for (const line of text.split('\n')) {
34
+ const trimmed = line.trim();
35
+ if (!trimmed || trimmed.startsWith('#')) continue;
36
+ const eq = trimmed.indexOf('=');
37
+ if (eq < 0) continue;
38
+ const key = trimmed.slice(0, eq).trim();
39
+ const value = trimmed.slice(eq + 1).trim();
40
+ map.set(key, value);
41
+ }
42
+ return { text, map };
43
+ } catch {
44
+ return { text: '', map: new Map() };
45
+ }
46
+ }
47
+
48
+ function ask(rl, question) {
49
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer)));
50
+ }
51
+
52
+ async function main() {
53
+ console.log(c('bold', '\n🔑 Environment setup\n'));
54
+
55
+ const { map: existing } = await readEnvFile(envLocalPath);
56
+ const { text: exampleText } = await readEnvFile(envExamplePath);
57
+ if (!exampleText) {
58
+ console.log(c('red', 'No .env.example found — re-run the scaffolder.'));
59
+ process.exit(1);
60
+ }
61
+
62
+ // Parse .env.example into sections by '# --- Heading ---' comments.
63
+ const sections = [];
64
+ let current = { heading: 'Other', keys: [] };
65
+ for (const line of exampleText.split('\n')) {
66
+ const trimmed = line.trim();
67
+ const headingMatch = trimmed.match(/^# --- (.*) ---/);
68
+ if (headingMatch) {
69
+ if (current.keys.length > 0) sections.push(current);
70
+ current = { heading: headingMatch[1], keys: [] };
71
+ continue;
72
+ }
73
+ if (!trimmed || trimmed.startsWith('#')) continue;
74
+ const eq = trimmed.indexOf('=');
75
+ if (eq < 0) continue;
76
+ const key = trimmed.slice(0, eq).trim();
77
+ const example = trimmed.slice(eq + 1).trim();
78
+ current.keys.push({ key, example });
79
+ }
80
+ if (current.keys.length > 0) sections.push(current);
81
+
82
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
83
+ const answers = new Map(existing);
84
+ let setCount = 0;
85
+ let skipCount = 0;
86
+
87
+ console.log(c('dim', `Found ${sections.reduce((acc, s) => acc + s.keys.length, 0)} env vars across ${sections.length} sections.`));
88
+ console.log(c('dim', `Existing values in .env.local will be kept; just hit Enter to skip them.\n`));
89
+
90
+ for (const section of sections) {
91
+ console.log(c('cyan', `\n${section.heading}`));
92
+ for (const { key, example } of section.keys) {
93
+ const current = answers.get(key) ?? '';
94
+ const prompt = current
95
+ ? ` ${c('bold', key)} ${c('dim', `(current: ${maskSecret(current)})`)}: `
96
+ : ` ${c('bold', key)} ${example ? c('dim', `(e.g. ${example})`) : ''}: `;
97
+ const answer = (await ask(rl, prompt)).trim();
98
+ if (answer) {
99
+ answers.set(key, answer);
100
+ setCount++;
101
+ } else if (!current) {
102
+ skipCount++;
103
+ }
104
+ }
105
+ }
106
+
107
+ rl.close();
108
+
109
+ // Write .env.local — preserve any existing keys not in the example.
110
+ const lines = [];
111
+ const written = new Set();
112
+ for (const section of sections) {
113
+ lines.push(`# --- ${section.heading} ---`);
114
+ for (const { key } of section.keys) {
115
+ lines.push(`${key}=${answers.get(key) ?? ''}`);
116
+ written.add(key);
117
+ }
118
+ lines.push('');
119
+ }
120
+ // Pass-through anything in .env.local that wasn't in the example.
121
+ const passthrough = [];
122
+ for (const [key, value] of answers.entries()) {
123
+ if (!written.has(key)) passthrough.push(`${key}=${value}`);
124
+ }
125
+ if (passthrough.length > 0) {
126
+ lines.push('# --- Other ---');
127
+ lines.push(...passthrough);
128
+ lines.push('');
129
+ }
130
+
131
+ await fs.writeFile(envLocalPath, lines.join('\n') + '\n', 'utf8');
132
+
133
+ console.log(c('green', `\n✓ Wrote .env.local`));
134
+ console.log(c('dim', ` ${setCount} set / ${skipCount} skipped\n`));
135
+ console.log(`Next: ${c('bold', 'pnpm dev')}\n`);
136
+ }
137
+
138
+ function maskSecret(value) {
139
+ if (value.length <= 8) return value;
140
+ return `${value.slice(0, 4)}…${value.slice(-4)}`;
141
+ }
142
+
143
+ main().catch((err) => {
144
+ console.error(c('red', '\n✕ Setup failed:'), err);
145
+ process.exit(1);
146
+ });
@@ -0,0 +1,68 @@
1
+ @import 'tailwindcss';
2
+
3
+ @theme {
4
+ --color-background: hsl(0 0% 100%);
5
+ --color-foreground: hsl(222.2 47.4% 11.2%);
6
+
7
+ --color-muted: hsl(210 40% 96.1%);
8
+ --color-muted-foreground: hsl(215.4 16.3% 46.9%);
9
+
10
+ --color-card: hsl(0 0% 100%);
11
+ --color-card-foreground: hsl(222.2 47.4% 11.2%);
12
+
13
+ --color-popover: hsl(0 0% 100%);
14
+ --color-popover-foreground: hsl(222.2 47.4% 11.2%);
15
+
16
+ --color-primary: hsl(222.2 47.4% 11.2%);
17
+ --color-primary-foreground: hsl(210 40% 98%);
18
+
19
+ --color-secondary: hsl(210 40% 96.1%);
20
+ --color-secondary-foreground: hsl(222.2 47.4% 11.2%);
21
+
22
+ --color-accent: hsl(210 40% 96.1%);
23
+ --color-accent-foreground: hsl(222.2 47.4% 11.2%);
24
+
25
+ --color-destructive: hsl(0 84.2% 60.2%);
26
+ --color-destructive-foreground: hsl(210 40% 98%);
27
+
28
+ --color-border: hsl(214.3 31.8% 91.4%);
29
+ --color-input: hsl(214.3 31.8% 91.4%);
30
+ --color-ring: hsl(222.2 84% 4.9%);
31
+
32
+ /* Primary brand — only --brand + --brand-hex are rewritten by the brand
33
+ installer; --color-brand reads --brand so Tailwind utilities like
34
+ bg-brand / text-brand pick up the override automatically. */
35
+ --brand: 199 89% 48%;
36
+ --brand-hex: #0ea5e9;
37
+ --color-brand: hsl(var(--brand));
38
+
39
+ /* Accent brand — same pattern. */
40
+ --brand-accent: 38 92% 50%;
41
+ --brand-accent-hex: #f59e0b;
42
+ --color-brand-accent: hsl(var(--brand-accent));
43
+
44
+ --radius: 0.5rem;
45
+ }
46
+
47
+ @media (prefers-color-scheme: dark) {
48
+ @theme {
49
+ --color-background: hsl(222.2 84% 4.9%);
50
+ --color-foreground: hsl(210 40% 98%);
51
+ --color-muted: hsl(217.2 32.6% 17.5%);
52
+ --color-muted-foreground: hsl(215 20.2% 65.1%);
53
+ --color-card: hsl(222.2 84% 4.9%);
54
+ --color-card-foreground: hsl(210 40% 98%);
55
+ --color-border: hsl(217.2 32.6% 17.5%);
56
+ --color-input: hsl(217.2 32.6% 17.5%);
57
+ --color-primary: hsl(210 40% 98%);
58
+ --color-primary-foreground: hsl(222.2 47.4% 11.2%);
59
+ }
60
+ }
61
+
62
+ * {
63
+ border-color: var(--color-border);
64
+ }
65
+
66
+ body {
67
+ font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
68
+ }
@@ -0,0 +1,19 @@
1
+ import type { Metadata } from 'next';
2
+ import './globals.css';
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'CRM',
6
+ description: 'A home-services CRM built with create-crm-starter',
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: Readonly<{ children: React.ReactNode }>) {
12
+ return (
13
+ <html lang="en">
14
+ <body className="bg-background text-foreground min-h-screen antialiased">
15
+ {children}
16
+ </body>
17
+ </html>
18
+ );
19
+ }
@@ -0,0 +1,37 @@
1
+ import Link from 'next/link';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4
+
5
+ export default function HomePage() {
6
+ return (
7
+ <main className="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center gap-8 p-8">
8
+ <header className="text-center">
9
+ <h1 className="text-4xl font-bold tracking-tight sm:text-5xl">Welcome to your CRM</h1>
10
+ <p className="text-muted-foreground mt-3 text-lg">
11
+ Generated by <code className="bg-muted rounded px-1.5 py-0.5">create-crm-starter</code>
12
+ </p>
13
+ </header>
14
+ <Card className="w-full">
15
+ <CardHeader>
16
+ <CardTitle>Next steps</CardTitle>
17
+ <CardDescription>Finish setup and explore the dashboard.</CardDescription>
18
+ </CardHeader>
19
+ <CardContent className="space-y-3 text-sm">
20
+ <p>
21
+ 1. Fill in <code className="bg-muted rounded px-1 py-0.5">.env.local</code> with your auth and database credentials.
22
+ </p>
23
+ <p>
24
+ 2. Sign up at <Link href="/sign-up" className="text-brand underline">/sign-up</Link> to create your first admin user.
25
+ </p>
26
+ <p>
27
+ 3. Head to <Link href="/dashboard" className="text-brand underline">/dashboard</Link> once authenticated.
28
+ </p>
29
+ </CardContent>
30
+ </Card>
31
+ <div className="flex gap-3">
32
+ <Button asChild><Link href="/sign-in">Sign in</Link></Button>
33
+ <Button asChild variant="outline"><Link href="/sign-up">Sign up</Link></Button>
34
+ </div>
35
+ </main>
36
+ );
37
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ const badgeVariants = cva(
6
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'border-transparent bg-primary text-primary-foreground',
11
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
12
+ destructive: 'border-transparent bg-destructive text-destructive-foreground',
13
+ outline: 'text-foreground',
14
+ brand: 'border-transparent bg-brand text-white',
15
+ },
16
+ },
17
+ defaultVariants: { variant: 'default' },
18
+ },
19
+ );
20
+
21
+ export interface BadgeProps
22
+ extends React.HTMLAttributes<HTMLDivElement>,
23
+ VariantProps<typeof badgeVariants> {}
24
+
25
+ function Badge({ className, variant, ...props }: BadgeProps) {
26
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
27
+ }
28
+
29
+ export { Badge, badgeVariants };
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const buttonVariants = cva(
7
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
12
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
13
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
14
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
15
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
16
+ link: 'text-brand underline-offset-4 hover:underline',
17
+ },
18
+ size: {
19
+ default: 'h-10 px-4 py-2',
20
+ sm: 'h-9 rounded-md px-3',
21
+ lg: 'h-11 rounded-md px-8',
22
+ icon: 'h-10 w-10',
23
+ },
24
+ },
25
+ defaultVariants: { variant: 'default', size: 'default' },
26
+ },
27
+ );
28
+
29
+ export interface ButtonProps
30
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
31
+ VariantProps<typeof buttonVariants> {
32
+ asChild?: boolean;
33
+ }
34
+
35
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
36
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
37
+ const Comp = asChild ? Slot : 'button';
38
+ return (
39
+ <Comp
40
+ className={cn(buttonVariants({ variant, size, className }))}
41
+ ref={ref}
42
+ {...props}
43
+ />
44
+ );
45
+ },
46
+ );
47
+ Button.displayName = 'Button';
48
+
49
+ export { Button, buttonVariants };
@@ -0,0 +1,54 @@
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <div
7
+ ref={ref}
8
+ className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
9
+ {...props}
10
+ />
11
+ ),
12
+ );
13
+ Card.displayName = 'Card';
14
+
15
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
16
+ ({ className, ...props }, ref) => (
17
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
18
+ ),
19
+ );
20
+ CardHeader.displayName = 'CardHeader';
21
+
22
+ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
23
+ ({ className, ...props }, ref) => (
24
+ <h3
25
+ ref={ref}
26
+ className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
27
+ {...props}
28
+ />
29
+ ),
30
+ );
31
+ CardTitle.displayName = 'CardTitle';
32
+
33
+ const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
34
+ ({ className, ...props }, ref) => (
35
+ <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
36
+ ),
37
+ );
38
+ CardDescription.displayName = 'CardDescription';
39
+
40
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
41
+ ({ className, ...props }, ref) => (
42
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
43
+ ),
44
+ );
45
+ CardContent.displayName = 'CardContent';
46
+
47
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
48
+ ({ className, ...props }, ref) => (
49
+ <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
50
+ ),
51
+ );
52
+ CardFooter.displayName = 'CardFooter';
53
+
54
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className, type, ...props }, ref) => (
6
+ <input
7
+ type={type}
8
+ className={cn(
9
+ 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
10
+ className,
11
+ )}
12
+ ref={ref}
13
+ {...props}
14
+ />
15
+ ),
16
+ );
17
+ Input.displayName = 'Input';
18
+
19
+ export { Input };
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ const Label = React.forwardRef<
5
+ HTMLLabelElement,
6
+ React.LabelHTMLAttributes<HTMLLabelElement>
7
+ >(({ className, ...props }, ref) => (
8
+ <label
9
+ ref={ref}
10
+ className={cn(
11
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ ));
17
+ Label.displayName = 'Label';
18
+
19
+ export { Label };
@@ -0,0 +1,25 @@
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ orientation?: 'horizontal' | 'vertical';
6
+ }
7
+
8
+ const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
9
+ ({ className, orientation = 'horizontal', ...props }, ref) => (
10
+ <div
11
+ ref={ref}
12
+ role="separator"
13
+ aria-orientation={orientation}
14
+ className={cn(
15
+ 'shrink-0 bg-border',
16
+ orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ ),
22
+ );
23
+ Separator.displayName = 'Separator';
24
+
25
+ export { Separator };
@@ -0,0 +1,7 @@
1
+ import { cn } from '@/lib/utils';
2
+
3
+ function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
4
+ return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />;
5
+ }
6
+
7
+ export { Skeleton };
@@ -0,0 +1,17 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export function formatCurrency(cents: number): string {
9
+ return new Intl.NumberFormat('en-US', {
10
+ style: 'currency',
11
+ currency: 'USD',
12
+ }).format(cents / 100);
13
+ }
14
+
15
+ export function formatDate(d: Date | string | number, opts: Intl.DateTimeFormatOptions = { dateStyle: 'medium' }): string {
16
+ return new Intl.DateTimeFormat('en-US', opts).format(new Date(d));
17
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "ES2022"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./src/*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules", "mobile"]
21
+ }
@@ -0,0 +1,7 @@
1
+ export default function AuthLayout({ children }: { children: React.ReactNode }) {
2
+ return (
3
+ <main className="bg-muted/30 flex min-h-screen items-center justify-center p-6">
4
+ <div className="w-full max-w-md">{children}</div>
5
+ </main>
6
+ );
7
+ }