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.
- package/README.md +53 -0
- package/dist/index.js +2341 -0
- package/package.json +51 -0
- package/template/base/_dot_gitignore +11 -0
- package/template/base/eslint.config.mjs +7 -0
- package/template/base/next.config.ts +17 -0
- package/template/base/package.json +35 -0
- package/template/base/postcss.config.mjs +7 -0
- package/template/base/scripts/setup.mjs +146 -0
- package/template/base/src/app/globals.css +68 -0
- package/template/base/src/app/layout.tsx +19 -0
- package/template/base/src/app/page.tsx +37 -0
- package/template/base/src/components/ui/badge.tsx +29 -0
- package/template/base/src/components/ui/button.tsx +49 -0
- package/template/base/src/components/ui/card.tsx +54 -0
- package/template/base/src/components/ui/input.tsx +19 -0
- package/template/base/src/components/ui/label.tsx +19 -0
- package/template/base/src/components/ui/separator.tsx +25 -0
- package/template/base/src/components/ui/skeleton.tsx +7 -0
- package/template/base/src/lib/utils.ts +17 -0
- package/template/base/tsconfig.json +21 -0
- package/template/extras/auth-better-auth/src/app/(auth)/layout.tsx +7 -0
- package/template/extras/auth-better-auth/src/app/(auth)/sign-in/page.tsx +65 -0
- package/template/extras/auth-better-auth/src/app/(auth)/sign-up/page.tsx +70 -0
- package/template/extras/auth-better-auth/src/app/(dashboard)/admin/page.tsx +68 -0
- package/template/extras/auth-better-auth/src/app/(dashboard)/dashboard/page.tsx +42 -0
- package/template/extras/auth-better-auth/src/app/(dashboard)/layout.tsx +41 -0
- package/template/extras/auth-better-auth/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/extras/auth-better-auth/src/components/sign-out-button.tsx +19 -0
- package/template/extras/auth-better-auth/src/lib/auth-client.ts +7 -0
- package/template/extras/auth-better-auth/src/lib/auth-server.ts +51 -0
- package/template/extras/auth-better-auth/src/lib/auth.ts +76 -0
- package/template/extras/auth-better-auth/src/middleware.ts +25 -0
- package/template/extras/auth-clerk/src/app/(auth)/layout.tsx +7 -0
- package/template/extras/auth-clerk/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +5 -0
- package/template/extras/auth-clerk/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +5 -0
- package/template/extras/auth-clerk/src/app/(dashboard)/admin/page.tsx +37 -0
- package/template/extras/auth-clerk/src/app/(dashboard)/dashboard/page.tsx +42 -0
- package/template/extras/auth-clerk/src/app/(dashboard)/layout.tsx +57 -0
- package/template/extras/auth-clerk/src/lib/auth.ts +55 -0
- package/template/extras/auth-clerk/src/middleware.ts +17 -0
- package/template/extras/calendar-dispatch/_shared/src/app/(dashboard)/calendar/page.tsx +39 -0
- package/template/extras/calendar-dispatch/drizzle/src/app/(dashboard)/calendar/page.tsx +21 -0
- package/template/extras/calendar-dispatch/drizzle/src/components/calendar/calendar-board.tsx +195 -0
- package/template/extras/calendar-dispatch/drizzle/src/lib/calendar/actions.ts +35 -0
- package/template/extras/calendar-dispatch/drizzle/src/lib/calendar/data.ts +74 -0
- package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/[id]/page.tsx +48 -0
- package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/new/page.tsx +15 -0
- package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/page.tsx +83 -0
- package/template/extras/checklists/_shared/src/components/jobs/job-checklists-section.tsx +18 -0
- package/template/extras/checklists/_shared/src/lib/checklists/data.ts +17 -0
- package/template/extras/checklists/_shared/src/lib/checklists/sample-data.ts +56 -0
- package/template/extras/checklists/_shared/src/lib/checklists/types.ts +47 -0
- package/template/extras/checklists/drizzle/src/app/(dashboard)/checklists/new/page.tsx +10 -0
- package/template/extras/checklists/drizzle/src/components/checklists/new-template-form.tsx +158 -0
- package/template/extras/checklists/drizzle/src/components/jobs/job-checklists-client.tsx +202 -0
- package/template/extras/checklists/drizzle/src/components/jobs/job-checklists-section.tsx +24 -0
- package/template/extras/checklists/drizzle/src/db/schema/checklists.ts +52 -0
- package/template/extras/checklists/drizzle/src/lib/checklists/actions.ts +112 -0
- package/template/extras/checklists/drizzle/src/lib/checklists/data.ts +80 -0
- package/template/extras/comms-email/src/app/(dashboard)/email/page.tsx +32 -0
- package/template/extras/comms-sms/_shared/src/app/(dashboard)/sms/new/page.tsx +35 -0
- package/template/extras/comms-sms/_shared/src/app/(dashboard)/sms/page.tsx +55 -0
- package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/[customerId]/page.tsx +102 -0
- package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/new/page.tsx +120 -0
- package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/page.tsx +70 -0
- package/template/extras/comms-sms/drizzle/src/app/api/twilio/sms/route.ts +69 -0
- package/template/extras/comms-sms/drizzle/src/db/schema/sms-messages.ts +27 -0
- package/template/extras/comms-sms/drizzle/src/lib/sms/actions.ts +107 -0
- package/template/extras/comms-sms/drizzle/src/lib/sms/data.ts +111 -0
- package/template/extras/customer-portal/_shared/src/app/portal/[token]/layout.tsx +51 -0
- package/template/extras/customer-portal/drizzle/src/app/portal/[token]/appointments/page.tsx +78 -0
- package/template/extras/customer-portal/drizzle/src/app/portal/[token]/estimates/page.tsx +94 -0
- package/template/extras/customer-portal/drizzle/src/app/portal/[token]/invoices/page.tsx +71 -0
- package/template/extras/customer-portal/drizzle/src/app/portal/[token]/page.tsx +118 -0
- package/template/extras/customer-portal/drizzle/src/components/portal/estimate-approval.tsx +141 -0
- package/template/extras/customer-portal/drizzle/src/components/portal/signature-pad.tsx +126 -0
- package/template/extras/customer-portal/drizzle/src/lib/portal/actions.ts +107 -0
- package/template/extras/customer-portal/drizzle/src/lib/portal/data.ts +158 -0
- package/template/extras/customers/_fragments/convex.txt +28 -0
- package/template/extras/customers/_shared/src/app/(dashboard)/customers/[id]/page.tsx +81 -0
- package/template/extras/customers/_shared/src/app/(dashboard)/customers/new/page.tsx +16 -0
- package/template/extras/customers/_shared/src/app/(dashboard)/customers/page.tsx +73 -0
- package/template/extras/customers/_shared/src/lib/customers/data.ts +15 -0
- package/template/extras/customers/_shared/src/lib/customers/sample-data.ts +67 -0
- package/template/extras/customers/_shared/src/lib/customers/types.ts +31 -0
- package/template/extras/customers/convex/convex/customers.ts +52 -0
- package/template/extras/customers/convex/src/lib/customers/data.ts +64 -0
- package/template/extras/customers/drizzle/src/app/(dashboard)/customers/new/page.tsx +82 -0
- package/template/extras/customers/drizzle/src/db/schema/customers.ts +34 -0
- package/template/extras/customers/drizzle/src/lib/customers/actions.ts +67 -0
- package/template/extras/customers/drizzle/src/lib/customers/data.ts +34 -0
- package/template/extras/db-convex/convex/_generated/README.md +13 -0
- package/template/extras/db-convex/convex/_generated/api.d.ts +8 -0
- package/template/extras/db-convex/convex/_generated/api.js +12 -0
- package/template/extras/db-convex/convex/_generated/dataModel.d.ts +9 -0
- package/template/extras/db-convex/convex/_generated/server.d.ts +18 -0
- package/template/extras/db-convex/convex/_generated/server.js +12 -0
- package/template/extras/db-convex/convex/auth.config.ts +17 -0
- package/template/extras/db-convex/convex/schema.ts +28 -0
- package/template/extras/db-convex/src/app/layout.tsx +20 -0
- package/template/extras/db-convex/src/components/providers.tsx +28 -0
- package/template/extras/db-convex/src/lib/convex.ts +6 -0
- package/template/extras/db-drizzle-pg/docker-compose.yml +21 -0
- package/template/extras/db-drizzle-pg/drizzle.config.ts +24 -0
- package/template/extras/db-drizzle-pg/src/app/layout.tsx +20 -0
- package/template/extras/db-drizzle-pg/src/components/providers.tsx +16 -0
- package/template/extras/db-drizzle-pg/src/db/client.ts +14 -0
- package/template/extras/db-drizzle-pg/src/db/schema/auth.ts +62 -0
- package/template/extras/db-drizzle-pg/src/db/schema/index.ts +3 -0
- package/template/extras/emergency-dispatch/src/app/(dashboard)/emergency/page.tsx +43 -0
- package/template/extras/equipment-tracking/src/app/(dashboard)/equipment/page.tsx +61 -0
- package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/[id]/page.tsx +123 -0
- package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/new/page.tsx +22 -0
- package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/page.tsx +102 -0
- package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/invoices/[id]/page.tsx +168 -0
- package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/invoices/page.tsx +100 -0
- package/template/extras/estimates-invoices/_shared/src/components/estimates/convert-to-job-button.tsx +14 -0
- package/template/extras/estimates-invoices/_shared/src/components/invoices/pay-invoice-button.tsx +15 -0
- package/template/extras/estimates-invoices/_shared/src/components/invoices/send-invoice-email-button.tsx +14 -0
- package/template/extras/estimates-invoices/_shared/src/lib/estimates/data.ts +14 -0
- package/template/extras/estimates-invoices/_shared/src/lib/estimates/sample-data.ts +74 -0
- package/template/extras/estimates-invoices/_shared/src/lib/estimates/types.ts +60 -0
- package/template/extras/estimates-invoices/_shared/src/lib/invoices/data.ts +14 -0
- package/template/extras/estimates-invoices/_shared/src/lib/invoices/sample-data.ts +83 -0
- package/template/extras/estimates-invoices/_shared/src/lib/invoices/types.ts +78 -0
- package/template/extras/estimates-invoices/drizzle/src/app/(dashboard)/estimates/new/page.tsx +18 -0
- package/template/extras/estimates-invoices/drizzle/src/app/api/stripe/webhook/route.ts +87 -0
- package/template/extras/estimates-invoices/drizzle/src/app/i/[token]/page.tsx +148 -0
- package/template/extras/estimates-invoices/drizzle/src/components/estimates/convert-to-job-button.tsx +18 -0
- package/template/extras/estimates-invoices/drizzle/src/components/estimates/new-estimate-form.tsx +261 -0
- package/template/extras/estimates-invoices/drizzle/src/components/invoices/pay-invoice-button.tsx +19 -0
- package/template/extras/estimates-invoices/drizzle/src/components/invoices/public-pay-button.tsx +20 -0
- package/template/extras/estimates-invoices/drizzle/src/components/invoices/send-invoice-email-button.tsx +37 -0
- package/template/extras/estimates-invoices/drizzle/src/components/jobs/generate-invoice-button.tsx +23 -0
- package/template/extras/estimates-invoices/drizzle/src/db/schema/estimates.ts +41 -0
- package/template/extras/estimates-invoices/drizzle/src/db/schema/invoices.ts +59 -0
- package/template/extras/estimates-invoices/drizzle/src/lib/estimates/actions.ts +110 -0
- package/template/extras/estimates-invoices/drizzle/src/lib/estimates/data.ts +57 -0
- package/template/extras/estimates-invoices/drizzle/src/lib/invoices/actions.ts +199 -0
- package/template/extras/estimates-invoices/drizzle/src/lib/invoices/data.ts +99 -0
- package/template/extras/estimates-invoices/drizzle/src/lib/invoices/email-actions.ts +102 -0
- package/template/extras/inspection-checklists/src/app/(dashboard)/inspections/page.tsx +60 -0
- package/template/extras/jobs/_fragments/convex.txt +21 -0
- package/template/extras/jobs/_shared/src/app/(dashboard)/jobs/[id]/page.tsx +102 -0
- package/template/extras/jobs/_shared/src/app/(dashboard)/jobs/page.tsx +72 -0
- package/template/extras/jobs/_shared/src/components/jobs/advance-status-button.tsx +21 -0
- package/template/extras/jobs/_shared/src/components/jobs/generate-invoice-button.tsx +15 -0
- package/template/extras/jobs/_shared/src/components/jobs/photo-gallery.tsx +17 -0
- package/template/extras/jobs/_shared/src/components/jobs/photos-section.tsx +18 -0
- package/template/extras/jobs/_shared/src/lib/jobs/data.ts +14 -0
- package/template/extras/jobs/_shared/src/lib/jobs/sample-data.ts +50 -0
- package/template/extras/jobs/_shared/src/lib/jobs/types.ts +62 -0
- package/template/extras/jobs/convex/convex/jobs.ts +46 -0
- package/template/extras/jobs/convex/src/lib/jobs/data.ts +65 -0
- package/template/extras/jobs/drizzle/src/app/(dashboard)/jobs/new/page.tsx +18 -0
- package/template/extras/jobs/drizzle/src/components/jobs/advance-status-button.tsx +34 -0
- package/template/extras/jobs/drizzle/src/components/jobs/new-job-form.tsx +275 -0
- package/template/extras/jobs/drizzle/src/components/jobs/photo-gallery.tsx +130 -0
- package/template/extras/jobs/drizzle/src/components/jobs/photos-section.tsx +7 -0
- package/template/extras/jobs/drizzle/src/db/schema/job-attachments.ts +26 -0
- package/template/extras/jobs/drizzle/src/db/schema/jobs.ts +29 -0
- package/template/extras/jobs/drizzle/src/lib/jobs/actions.ts +71 -0
- package/template/extras/jobs/drizzle/src/lib/jobs/data.ts +48 -0
- package/template/extras/jobs/drizzle/src/lib/jobs/photos-actions.ts +121 -0
- package/template/extras/jobs/drizzle/src/lib/r2.ts +45 -0
- package/template/extras/landing-page/_shared/src/app/book/page.tsx +43 -0
- package/template/extras/landing-page/_shared/src/app/book/thanks/page.tsx +31 -0
- package/template/extras/landing-page/_shared/src/app/page.tsx +81 -0
- package/template/extras/landing-page/drizzle/src/app/book/page.tsx +105 -0
- package/template/extras/landing-page/drizzle/src/lib/booking/actions.ts +97 -0
- package/template/extras/maintenance-plans/src/app/(dashboard)/maintenance-plans/page.tsx +72 -0
- package/template/extras/mobile/mobile/README.md +67 -0
- package/template/extras/mobile/mobile/_dot_env.example +5 -0
- package/template/extras/mobile/mobile/_dot_gitignore +26 -0
- package/template/extras/mobile/mobile/app/(app)/_layout.tsx +37 -0
- package/template/extras/mobile/mobile/app/(app)/estimate.tsx +135 -0
- package/template/extras/mobile/mobile/app/(app)/inbox/[customerId].tsx +103 -0
- package/template/extras/mobile/mobile/app/(app)/inbox/index.tsx +70 -0
- package/template/extras/mobile/mobile/app/(app)/index.tsx +111 -0
- package/template/extras/mobile/mobile/app/(app)/job/[id]/checklist.tsx +99 -0
- package/template/extras/mobile/mobile/app/(app)/job/[id]/invoice.tsx +143 -0
- package/template/extras/mobile/mobile/app/(app)/job/[id].tsx +259 -0
- package/template/extras/mobile/mobile/app/_layout.tsx +14 -0
- package/template/extras/mobile/mobile/app/index.tsx +23 -0
- package/template/extras/mobile/mobile/app/sign-in.tsx +101 -0
- package/template/extras/mobile/mobile/app.brand.ts +14 -0
- package/template/extras/mobile/mobile/app.config.ts +40 -0
- package/template/extras/mobile/mobile/app.features.ts +11 -0
- package/template/extras/mobile/mobile/components/SignaturePad.tsx +60 -0
- package/template/extras/mobile/mobile/eas.json +22 -0
- package/template/extras/mobile/mobile/lib/api.ts +253 -0
- package/template/extras/mobile/mobile/lib/auth.ts +51 -0
- package/template/extras/mobile/mobile/lib/format.ts +23 -0
- package/template/extras/mobile/mobile/lib/push.ts +24 -0
- package/template/extras/mobile/mobile/lib/theme.ts +16 -0
- package/template/extras/mobile/mobile/package.json +34 -0
- package/template/extras/mobile/mobile/tsconfig.json +11 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/customers/[id]/route.ts +18 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/devices/route.ts +40 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/attachments/route.ts +59 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/checklists/item/route.ts +34 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/checklists/route.ts +15 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/route.ts +35 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/status/route.ts +28 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/clock-in/route.ts +35 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/clock-out/route.ts +27 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/route.ts +36 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/route.ts +30 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/me/route.ts +26 -0
- package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/uploads/sign/route.ts +46 -0
- package/template/extras/mobile-api/drizzle/src/db/schema/push-tokens.ts +21 -0
- package/template/extras/mobile-api/drizzle/src/db/schema/time-entries.ts +23 -0
- package/template/extras/mobile-api/drizzle/src/lib/mobile/cors.ts +30 -0
- package/template/extras/mobile-api/drizzle/src/lib/mobile/guard.ts +49 -0
- package/template/extras/mobile-api/drizzle/src/lib/push/send.ts +56 -0
- package/template/extras/mobile-api/estimates/src/app/api/mobile/v1/estimates/route.ts +52 -0
- package/template/extras/mobile-api/estimates/src/app/api/mobile/v1/price-book/route.ts +14 -0
- package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/invoices/[id]/payments/route.ts +32 -0
- package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/invoices/[id]/route.ts +28 -0
- package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/jobs/[id]/invoice/route.ts +82 -0
- package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/send/route.ts +32 -0
- package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/threads/[customerId]/route.ts +15 -0
- package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/threads/route.ts +14 -0
- package/template/extras/payments-stripe/src/app/(dashboard)/payments/page.tsx +56 -0
- package/template/extras/payments-stripe/src/app/api/stripe/webhook/route.ts +56 -0
- package/template/extras/payments-stripe/src/components/payments/demo-checkout-button.tsx +18 -0
- package/template/extras/payments-stripe/src/lib/payments/actions.ts +63 -0
- package/template/extras/payments-stripe/src/lib/stripe.ts +25 -0
- package/template/extras/permit-tracking/src/app/(dashboard)/permits/page.tsx +56 -0
- package/template/extras/price-book/_shared/src/app/(dashboard)/price-book/new/page.tsx +20 -0
- package/template/extras/price-book/_shared/src/app/(dashboard)/price-book/page.tsx +79 -0
- package/template/extras/price-book/_shared/src/components/price-book/item-picker.tsx +145 -0
- package/template/extras/price-book/_shared/src/lib/price-book/actions.ts +14 -0
- package/template/extras/price-book/_shared/src/lib/price-book/data.ts +28 -0
- package/template/extras/price-book/_shared/src/lib/price-book/sample-data.ts +62 -0
- package/template/extras/price-book/_shared/src/lib/price-book/types.ts +35 -0
- package/template/extras/price-book/drizzle/src/app/(dashboard)/price-book/new/page.tsx +18 -0
- package/template/extras/price-book/drizzle/src/components/price-book/new-item-form.tsx +254 -0
- package/template/extras/price-book/drizzle/src/db/schema/price-book.ts +33 -0
- package/template/extras/price-book/drizzle/src/lib/price-book/actions.ts +72 -0
- package/template/extras/price-book/drizzle/src/lib/price-book/data.ts +81 -0
- package/template/extras/reporting/src/app/(dashboard)/reports/page.tsx +66 -0
- package/template/extras/reviews/src/app/(dashboard)/reviews/page.tsx +58 -0
- package/template/extras/seed/_shared/scripts/seed.ts +35 -0
- package/template/extras/seed/drizzle/scripts/seed.ts +314 -0
- package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/[id]/page.tsx +114 -0
- package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/new/page.tsx +18 -0
- package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/page.tsx +92 -0
- package/template/extras/service-plans/_shared/src/components/service-plans/subscribe-customer-section.tsx +17 -0
- package/template/extras/service-plans/_shared/src/lib/service-plans/data.ts +14 -0
- package/template/extras/service-plans/_shared/src/lib/service-plans/sample-data.ts +83 -0
- package/template/extras/service-plans/_shared/src/lib/service-plans/types.ts +57 -0
- package/template/extras/service-plans/drizzle/src/app/(dashboard)/service-plans/new/page.tsx +10 -0
- package/template/extras/service-plans/drizzle/src/app/api/stripe/webhook/route.ts +143 -0
- package/template/extras/service-plans/drizzle/src/components/service-plans/new-plan-form.tsx +126 -0
- package/template/extras/service-plans/drizzle/src/components/service-plans/subscribe-customer-form.tsx +88 -0
- package/template/extras/service-plans/drizzle/src/components/service-plans/subscribe-customer-section.tsx +12 -0
- package/template/extras/service-plans/drizzle/src/db/schema/service-plans.ts +46 -0
- package/template/extras/service-plans/drizzle/src/lib/service-plans/actions.ts +196 -0
- 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,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,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,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
|
+
}
|