create-ej-app 25.0.0 → 26.0.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/cli.js +4 -0
- package/cli.ts +5 -0
- package/package.json +2 -2
- package/template/api/package.json +2 -2
- package/template/nextjs/gitignore +1 -0
- package/template/nextjs/package.json +18 -18
- package/template/saas-starter/.dockerignore +7 -0
- package/template/saas-starter/.env.example +25 -0
- package/template/saas-starter/.vscode/extensions.json +11 -0
- package/template/saas-starter/.vscode/settings.json +3 -0
- package/template/saas-starter/Dockerfile +15 -0
- package/template/saas-starter/README.md +3 -0
- package/template/saas-starter/components.json +25 -0
- package/template/saas-starter/drizzle.config.ts +10 -0
- package/template/saas-starter/eslint.config.mjs +23 -0
- package/template/saas-starter/gitignore +9 -0
- package/template/saas-starter/next-env.d.ts +6 -0
- package/template/saas-starter/next.config.ts +14 -0
- package/template/saas-starter/package.json +92 -0
- package/template/saas-starter/postcss.config.mjs +7 -0
- package/template/saas-starter/prettier.config.mjs +11 -0
- package/template/saas-starter/public/placeholder.svg +1 -0
- package/template/saas-starter/src/actions/uploads.ts +28 -0
- package/template/saas-starter/src/actions/uploads.validation.ts +14 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +97 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +21 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +40 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +57 -0
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +3 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +183 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +129 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +415 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +15 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +152 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +163 -0
- package/template/saas-starter/src/app/(app)/actions/organization.ts +92 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +20 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +34 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +100 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +364 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +155 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +232 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +116 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +169 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +571 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +40 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +69 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +29 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +17 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +27 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +41 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +140 -0
- package/template/saas-starter/src/app/(app)/app/layout.tsx +52 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +129 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +42 -0
- package/template/saas-starter/src/app/(app)/app/page.tsx +81 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +109 -0
- package/template/saas-starter/src/app/(auth)/login/page.tsx +87 -0
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +75 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +88 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +167 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +14 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +3 -0
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +3 -0
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +3 -0
- package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
- package/template/saas-starter/src/app/favicon.ico +0 -0
- package/template/saas-starter/src/app/globals.css +240 -0
- package/template/saas-starter/src/app/layout.tsx +53 -0
- package/template/saas-starter/src/app/not-found.tsx +27 -0
- package/template/saas-starter/src/app/proxy.ts +28 -0
- package/template/saas-starter/src/app/robots.ts +32 -0
- package/template/saas-starter/src/app/sitemap.ts +20 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +64 -0
- package/template/saas-starter/src/components/data-table/enum.ts +5 -0
- package/template/saas-starter/src/components/data-table/index.tsx +960 -0
- package/template/saas-starter/src/components/data-table/types.ts +75 -0
- package/template/saas-starter/src/components/error.tsx +26 -0
- package/template/saas-starter/src/components/file-upload.tsx +215 -0
- package/template/saas-starter/src/components/impersonation-banner.tsx +44 -0
- package/template/saas-starter/src/components/loader.tsx +25 -0
- package/template/saas-starter/src/components/navigation-search.tsx +140 -0
- package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
- package/template/saas-starter/src/components/page-heading.tsx +32 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +12 -0
- package/template/saas-starter/src/components/screen-size.tsx +40 -0
- package/template/saas-starter/src/components/site-footer.tsx +23 -0
- package/template/saas-starter/src/components/theme-provider.tsx +67 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +22 -0
- package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
- package/template/saas-starter/src/components/ui/badge.tsx +49 -0
- package/template/saas-starter/src/components/ui/button.tsx +67 -0
- package/template/saas-starter/src/components/ui/card.tsx +103 -0
- package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
- package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
- package/template/saas-starter/src/components/ui/command.tsx +192 -0
- package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
- package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/saas-starter/src/components/ui/field.tsx +238 -0
- package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
- package/template/saas-starter/src/components/ui/input.tsx +19 -0
- package/template/saas-starter/src/components/ui/label.tsx +24 -0
- package/template/saas-starter/src/components/ui/popover.tsx +89 -0
- package/template/saas-starter/src/components/ui/select.tsx +195 -0
- package/template/saas-starter/src/components/ui/separator.tsx +28 -0
- package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
- package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
- package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
- package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
- package/template/saas-starter/src/components/ui/switch.tsx +33 -0
- package/template/saas-starter/src/components/ui/table.tsx +116 -0
- package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
- package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
- package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
- package/template/saas-starter/src/components/user-menu.tsx +90 -0
- package/template/saas-starter/src/db/common.ts +31 -0
- package/template/saas-starter/src/db/enums.ts +26 -0
- package/template/saas-starter/src/db/index.ts +51 -0
- package/template/saas-starter/src/db/relations.ts +75 -0
- package/template/saas-starter/src/db/schema.ts +191 -0
- package/template/saas-starter/src/emails/components/email-layout.tsx +63 -0
- package/template/saas-starter/src/emails/invitation.tsx +56 -0
- package/template/saas-starter/src/emails/magic-link.tsx +49 -0
- package/template/saas-starter/src/emails/test.tsx +32 -0
- package/template/saas-starter/src/emails/welcome.tsx +34 -0
- package/template/saas-starter/src/env.ts +66 -0
- package/template/saas-starter/src/hooks/use-mobile.ts +19 -0
- package/template/saas-starter/src/instrumentation.ts +26 -0
- package/template/saas-starter/src/lib/actions.ts +21 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +31 -0
- package/template/saas-starter/src/lib/app-navigation.ts +59 -0
- package/template/saas-starter/src/lib/auth-client.ts +29 -0
- package/template/saas-starter/src/lib/auth-permissions.ts +22 -0
- package/template/saas-starter/src/lib/auth.ts +211 -0
- package/template/saas-starter/src/lib/billing.ts +117 -0
- package/template/saas-starter/src/lib/cn.ts +4 -0
- package/template/saas-starter/src/lib/constants.ts +22 -0
- package/template/saas-starter/src/lib/delay.ts +2 -0
- package/template/saas-starter/src/lib/email-service.ts +89 -0
- package/template/saas-starter/src/lib/format-currency.ts +27 -0
- package/template/saas-starter/src/lib/format-date.ts +12 -0
- package/template/saas-starter/src/lib/nodemailer.ts +43 -0
- package/template/saas-starter/src/lib/organization-access.ts +93 -0
- package/template/saas-starter/src/lib/organization-plan-limits.ts +38 -0
- package/template/saas-starter/src/lib/otel.ts +27 -0
- package/template/saas-starter/src/lib/plans.ts +63 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +75 -0
- package/template/saas-starter/src/lib/safe-action.ts +67 -0
- package/template/saas-starter/src/lib/siteConfig.ts +4 -0
- package/template/saas-starter/src/lib/storage.ts +181 -0
- package/template/saas-starter/src/lib/stripe.ts +6 -0
- package/template/saas-starter/src/lib/toast-message.ts +15 -0
- package/template/saas-starter/src/lib/validations.ts +12 -0
- package/template/saas-starter/src/scripts/reset-db.ts +22 -0
- package/template/saas-starter/src/scripts/seed.ts +11 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +200 -0
- package/template/saas-starter/src/scripts/test-mail.ts +35 -0
- package/template/saas-starter/tsconfig.json +51 -0
package/cli.js
CHANGED
|
@@ -65,6 +65,10 @@ program.action(async () => {
|
|
|
65
65
|
name: 'nextjs',
|
|
66
66
|
message: 'Full Stack setup with Next 16, Better Auth, shadcn and Drizzle'
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
name: 'saas-starter',
|
|
70
|
+
message: 'SaaS Starter with Stripe, Next 16, Better Auth, shadcn and Drizzle'
|
|
71
|
+
},
|
|
68
72
|
{
|
|
69
73
|
name: 'api',
|
|
70
74
|
message: 'API Server setup with Elysia, Drizzle, Typescript'
|
package/cli.ts
CHANGED
|
@@ -78,6 +78,11 @@ program.action(async () => {
|
|
|
78
78
|
message:
|
|
79
79
|
'Full Stack setup with Next 16, Better Auth, shadcn and Drizzle'
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
name: 'saas-starter',
|
|
83
|
+
message:
|
|
84
|
+
'SaaS Starter with Stripe, Next 16, Better Auth, shadcn and Drizzle'
|
|
85
|
+
},
|
|
81
86
|
{
|
|
82
87
|
name: 'api',
|
|
83
88
|
message: 'API Server setup with Elysia, Drizzle, Typescript'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ej-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "26.0.0",
|
|
4
4
|
"description": "Bootstrap a next.js app with my preferred setup",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"enquirer": "^2.4.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^25.9.
|
|
39
|
+
"@types/node": "^25.9.3",
|
|
40
40
|
"typescript": "^6.0.3"
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/bun": "1.3.14",
|
|
25
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
25
|
+
"@typescript/native-preview": "7.0.0-dev.20260614.1",
|
|
26
26
|
"drizzle-kit": "0.31.10",
|
|
27
27
|
"postgres": "3.4.9",
|
|
28
|
-
"prettier": "3.8.
|
|
28
|
+
"prettier": "3.8.4",
|
|
29
29
|
"prettier-plugin-organize-imports": "4.3.0"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -23,53 +23,53 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@ejekanshjain/simple-email-validator": "1.0.4",
|
|
25
25
|
"@hookform/resolvers": "5.4.0",
|
|
26
|
-
"@next/third-parties": "16.2.
|
|
26
|
+
"@next/third-parties": "16.2.9",
|
|
27
27
|
"@opentelemetry/api": "1.9.1",
|
|
28
|
-
"@opentelemetry/sdk-trace-base": "2.
|
|
28
|
+
"@opentelemetry/sdk-trace-base": "2.8.0",
|
|
29
29
|
"@paralleldrive/cuid2": "3.3.0",
|
|
30
30
|
"@react-email/render": "2.0.8",
|
|
31
31
|
"@t3-oss/env-nextjs": "0.13.11",
|
|
32
32
|
"@tanstack/react-query": "5.101.0",
|
|
33
|
-
"@vercel/otel": "2.1.
|
|
34
|
-
"better-auth": "1.6.
|
|
33
|
+
"@vercel/otel": "2.1.3",
|
|
34
|
+
"better-auth": "1.6.18",
|
|
35
35
|
"class-variance-authority": "0.7.1",
|
|
36
36
|
"clsx": "2.1.1",
|
|
37
37
|
"drizzle-orm": "0.45.2",
|
|
38
|
-
"lucide-react": "1.
|
|
39
|
-
"next": "16.2.
|
|
40
|
-
"next-safe-action": "8.5.
|
|
38
|
+
"lucide-react": "1.18.0",
|
|
39
|
+
"next": "16.2.9",
|
|
40
|
+
"next-safe-action": "8.5.4",
|
|
41
41
|
"next-themes": "^0.4.6",
|
|
42
42
|
"nextjs-toploader": "3.9.17",
|
|
43
|
-
"nodemailer": "
|
|
43
|
+
"nodemailer": "9.0.0",
|
|
44
44
|
"postgres": "3.4.9",
|
|
45
45
|
"radix-ui": "1.5.0",
|
|
46
46
|
"react": "19.2.7",
|
|
47
47
|
"react-dom": "19.2.7",
|
|
48
|
-
"react-hook-form": "7.
|
|
48
|
+
"react-hook-form": "7.79.0",
|
|
49
49
|
"sonner": "^2.0.7",
|
|
50
50
|
"tailwind-merge": "3.6.0",
|
|
51
51
|
"tw-animate-css": "1.4.0",
|
|
52
|
-
"workflow": "4.
|
|
52
|
+
"workflow": "4.4.0",
|
|
53
53
|
"zod": "4.4.3"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@next/bundle-analyzer": "16.2.
|
|
56
|
+
"@next/bundle-analyzer": "16.2.9",
|
|
57
57
|
"@react-email/preview-server": "5.2.10",
|
|
58
|
-
"@tailwindcss/postcss": "4.3.
|
|
58
|
+
"@tailwindcss/postcss": "4.3.1",
|
|
59
59
|
"@types/bun": "1.3.14",
|
|
60
|
-
"@types/nodemailer": "8.0.
|
|
60
|
+
"@types/nodemailer": "8.0.1",
|
|
61
61
|
"@types/react": "19.2.17",
|
|
62
62
|
"@types/react-dom": "19.2.3",
|
|
63
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
63
|
+
"@typescript/native-preview": "7.0.0-dev.20260614.1",
|
|
64
64
|
"babel-plugin-react-compiler": "1.0.0",
|
|
65
65
|
"drizzle-kit": "0.31.10",
|
|
66
66
|
"eslint": "9.39.4",
|
|
67
|
-
"eslint-config-next": "16.2.
|
|
68
|
-
"prettier": "3.8.
|
|
67
|
+
"eslint-config-next": "16.2.9",
|
|
68
|
+
"prettier": "3.8.4",
|
|
69
69
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
70
70
|
"prettier-plugin-tailwindcss": "0.8.0",
|
|
71
|
-
"react-email": "6.
|
|
72
|
-
"tailwindcss": "4.3.
|
|
71
|
+
"react-email": "6.6.0",
|
|
72
|
+
"tailwindcss": "4.3.1",
|
|
73
73
|
"typescript": "6.0.3"
|
|
74
74
|
},
|
|
75
75
|
"trustedDependencies": [
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
APP_ENV="production"
|
|
2
|
+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
|
3
|
+
BETTER_AUTH_URL="http://localhost:3000"
|
|
4
|
+
BETTER_AUTH_SECRET="secret"
|
|
5
|
+
BETTER_AUTH_GITHUB_ID="github_id"
|
|
6
|
+
BETTER_AUTH_GITHUB_SECRET="github_secret"
|
|
7
|
+
BETTER_AUTH_GOOGLE_ID="google_id"
|
|
8
|
+
BETTER_AUTH_GOOGLE_SECRET="google_secret"
|
|
9
|
+
EMAIL_SERVER_USER="username"
|
|
10
|
+
EMAIL_SERVER_PASSWORD="password"
|
|
11
|
+
EMAIL_SERVER_HOST="smtp.example.com"
|
|
12
|
+
EMAIL_SERVER_PORT="465"
|
|
13
|
+
EMAIL_FROM="no-reply@example.com"
|
|
14
|
+
STRIPE_SECRET_KEY="sk_test_xxxxxxxxxx"
|
|
15
|
+
STRIPE_WEBHOOK_SECRET="whsec_xxxxxxxxxx"
|
|
16
|
+
STRIPE_PRICE_PRO_MONTHLY="price_xxxxxxxxxx"
|
|
17
|
+
STRIPE_PRICE_PRO_ANNUAL="price_xxxxxxxxxx"
|
|
18
|
+
STRIPE_PRICE_ULTIMATE_MONTHLY="price_xxxxxxxxxx"
|
|
19
|
+
STRIPE_PRICE_ULTIMATE_ANNUAL="price_xxxxxxxxxx"
|
|
20
|
+
STRIPE_PORTAL_CONFIGURATION_ID="bpc_xxxxxxxxxx"
|
|
21
|
+
R2_ACCOUNT_ID=""
|
|
22
|
+
R2_ACCESS_KEY_ID=""
|
|
23
|
+
R2_SECRET_ACCESS_KEY=""
|
|
24
|
+
R2_BUCKET_NAME=""
|
|
25
|
+
R2_PUBLIC_URL=""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "radix-vega",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"rtl": false,
|
|
15
|
+
"aliases": {
|
|
16
|
+
"components": "~/components",
|
|
17
|
+
"utils": "~/lib/cn",
|
|
18
|
+
"ui": "~/components/ui",
|
|
19
|
+
"lib": "~/lib",
|
|
20
|
+
"hooks": "~/hooks"
|
|
21
|
+
},
|
|
22
|
+
"menuColor": "default",
|
|
23
|
+
"menuAccent": "subtle",
|
|
24
|
+
"registries": {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import nextVitals from 'eslint-config-next/core-web-vitals'
|
|
3
|
+
// import nextTs from 'eslint-config-next/typescript'
|
|
4
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
5
|
+
|
|
6
|
+
const eslintConfig = defineConfig([
|
|
7
|
+
...nextVitals,
|
|
8
|
+
// ...nextTs,
|
|
9
|
+
globalIgnores([
|
|
10
|
+
'node_modules/**',
|
|
11
|
+
'.next/**',
|
|
12
|
+
'out/**',
|
|
13
|
+
'build/**',
|
|
14
|
+
'next-env.d.ts'
|
|
15
|
+
]),
|
|
16
|
+
{
|
|
17
|
+
rules: {
|
|
18
|
+
'react/no-unescaped-entities': 'off'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
export default eslintConfig
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import bundleAnalyzer from '@next/bundle-analyzer'
|
|
2
|
+
import type { NextConfig } from 'next'
|
|
3
|
+
import { withWorkflow } from 'workflow/next'
|
|
4
|
+
import { env } from '~/env'
|
|
5
|
+
|
|
6
|
+
const nextConfig: NextConfig = {
|
|
7
|
+
reactCompiler: true
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const withBundleAnalyzer = bundleAnalyzer({
|
|
11
|
+
enabled: env.ANALYZE === 'true'
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default withWorkflow(withBundleAnalyzer(nextConfig))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-project",
|
|
3
|
+
"description": "A Legendary Project",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"email:dev": "email dev --dir src/emails",
|
|
9
|
+
"email:test": "bun ./src/scripts/test-mail.ts",
|
|
10
|
+
"workflow:inspect": "workflow inspect runs --web",
|
|
11
|
+
"build": "bun --bun next build",
|
|
12
|
+
"build:docker": "docker build -t my-app .",
|
|
13
|
+
"analyze": "ANALYZE=true bun --bun next build --webpack",
|
|
14
|
+
"start": "bun --bun next start",
|
|
15
|
+
"lint": "eslint",
|
|
16
|
+
"type-check": "tsgo --incremental false",
|
|
17
|
+
"format": "prettier --write \"./src/**/*.{js,cjs,mjs,jsx,ts,tsx}\"",
|
|
18
|
+
"db:push": "drizzle-kit push",
|
|
19
|
+
"db:seed": "bun ./src/scripts/seed.ts",
|
|
20
|
+
"db:reset": "bun ./src/scripts/reset-db.ts",
|
|
21
|
+
"db:studio": "drizzle-kit studio",
|
|
22
|
+
"gen:auth": "bunx @better-auth/cli generate",
|
|
23
|
+
"stripe:setup": "bun ./src/scripts/setup-stripe-plans.ts",
|
|
24
|
+
"stripe:dev": "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@aws-sdk/client-s3": "3.1068.0",
|
|
28
|
+
"@aws-sdk/s3-request-presigner": "3.1068.0",
|
|
29
|
+
"@ejekanshjain/simple-email-validator": "1.0.4",
|
|
30
|
+
"@hookform/resolvers": "5.4.0",
|
|
31
|
+
"@next/third-parties": "16.2.9",
|
|
32
|
+
"@opentelemetry/api": "1.9.1",
|
|
33
|
+
"@opentelemetry/sdk-trace-base": "2.8.0",
|
|
34
|
+
"@paralleldrive/cuid2": "3.3.0",
|
|
35
|
+
"@react-email/render": "2.0.8",
|
|
36
|
+
"@t3-oss/env-nextjs": "0.13.11",
|
|
37
|
+
"@tanstack/react-query": "5.101.0",
|
|
38
|
+
"@tanstack/react-table": "8.21.3",
|
|
39
|
+
"@vercel/otel": "2.1.3",
|
|
40
|
+
"better-auth": "1.6.18",
|
|
41
|
+
"class-variance-authority": "0.7.1",
|
|
42
|
+
"clsx": "2.1.1",
|
|
43
|
+
"cmdk": "1.1.1",
|
|
44
|
+
"drizzle-orm": "0.45.2",
|
|
45
|
+
"lucide-react": "1.18.0",
|
|
46
|
+
"next": "16.2.9",
|
|
47
|
+
"next-safe-action": "8.5.4",
|
|
48
|
+
"next-themes": "0.4.6",
|
|
49
|
+
"nextjs-toploader": "3.9.17",
|
|
50
|
+
"nodemailer": "9.0.0",
|
|
51
|
+
"nuqs": "2.8.9",
|
|
52
|
+
"postgres": "3.4.9",
|
|
53
|
+
"radix-ui": "1.5.0",
|
|
54
|
+
"react": "19.2.7",
|
|
55
|
+
"react-dom": "19.2.7",
|
|
56
|
+
"react-hook-form": "7.79.0",
|
|
57
|
+
"sonner": "2.0.7",
|
|
58
|
+
"stripe": "22.2.1",
|
|
59
|
+
"tailwind-merge": "3.6.0",
|
|
60
|
+
"tw-animate-css": "1.4.0",
|
|
61
|
+
"workflow": "4.4.0",
|
|
62
|
+
"zod": "4.4.3"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@next/bundle-analyzer": "16.2.9",
|
|
66
|
+
"@react-email/preview-server": "5.2.10",
|
|
67
|
+
"@tailwindcss/postcss": "4.3.1",
|
|
68
|
+
"@types/bun": "1.3.14",
|
|
69
|
+
"@types/nodemailer": "8.0.1",
|
|
70
|
+
"@types/react": "19.2.17",
|
|
71
|
+
"@types/react-dom": "19.2.3",
|
|
72
|
+
"@typescript/native-preview": "7.0.0-dev.20260614.1",
|
|
73
|
+
"babel-plugin-react-compiler": "1.0.0",
|
|
74
|
+
"drizzle-kit": "0.31.10",
|
|
75
|
+
"eslint": "9.39.4",
|
|
76
|
+
"eslint-config-next": "16.2.9",
|
|
77
|
+
"prettier": "3.8.4",
|
|
78
|
+
"prettier-plugin-organize-imports": "4.3.0",
|
|
79
|
+
"prettier-plugin-tailwindcss": "0.8.0",
|
|
80
|
+
"react-email": "6.6.0",
|
|
81
|
+
"tailwindcss": "4.3.1",
|
|
82
|
+
"typescript": "6.0.3"
|
|
83
|
+
},
|
|
84
|
+
"trustedDependencies": [
|
|
85
|
+
"@nestjs/core",
|
|
86
|
+
"@swc/core",
|
|
87
|
+
"cbor-extract",
|
|
88
|
+
"esbuild",
|
|
89
|
+
"sharp",
|
|
90
|
+
"unrs-resolver"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
|
|
2
|
+
const config = {
|
|
3
|
+
plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
|
|
4
|
+
arrowParens: 'avoid',
|
|
5
|
+
trailingComma: 'none',
|
|
6
|
+
semi: false,
|
|
7
|
+
singleQuote: true,
|
|
8
|
+
tabWidth: 2
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default config
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { assertUserMembership } from '~/lib/organization-access'
|
|
4
|
+
import { authActionClient } from '~/lib/safe-action'
|
|
5
|
+
import { generateUploadUrl, isR2Configured } from '~/lib/storage'
|
|
6
|
+
import { generateUploadUrlSchema } from './uploads.validation'
|
|
7
|
+
|
|
8
|
+
export const generateUploadUrlAction = authActionClient
|
|
9
|
+
.inputSchema(generateUploadUrlSchema)
|
|
10
|
+
.action(async ({ parsedInput, ctx: { user } }) => {
|
|
11
|
+
if (!isR2Configured()) {
|
|
12
|
+
return { mode: 'base64' as const }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (parsedInput.organizationId) {
|
|
16
|
+
await assertUserMembership(parsedInput.organizationId)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { uploadUrl, key } = await generateUploadUrl({
|
|
20
|
+
filename: parsedInput.filename,
|
|
21
|
+
mimeType: parsedInput.mimeType,
|
|
22
|
+
size: parsedInput.size,
|
|
23
|
+
uploadedBy: user.id,
|
|
24
|
+
organizationId: parsedInput.organizationId
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return { mode: 'r2' as const, uploadUrl, key }
|
|
28
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { MAX_FILE_SIZE_BYTES, MAX_FILE_SIZE_MB } from '~/lib/constants'
|
|
3
|
+
import { stringValidation } from '~/lib/validations'
|
|
4
|
+
|
|
5
|
+
export const generateUploadUrlSchema = z.object({
|
|
6
|
+
filename: stringValidation,
|
|
7
|
+
mimeType: stringValidation,
|
|
8
|
+
size: z
|
|
9
|
+
.number()
|
|
10
|
+
.int()
|
|
11
|
+
.positive()
|
|
12
|
+
.max(MAX_FILE_SIZE_BYTES, `File must be under ${MAX_FILE_SIZE_MB}MB`),
|
|
13
|
+
organizationId: stringValidation
|
|
14
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
and,
|
|
5
|
+
asc,
|
|
6
|
+
count,
|
|
7
|
+
desc,
|
|
8
|
+
ilike,
|
|
9
|
+
inArray,
|
|
10
|
+
isNotNull,
|
|
11
|
+
or
|
|
12
|
+
} from 'drizzle-orm'
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
import { SortOrderEnum } from '~/components/data-table/enum'
|
|
15
|
+
import { db } from '~/db'
|
|
16
|
+
import { usersTable } from '~/db/schema'
|
|
17
|
+
import { getPaginationSchema, getSortSchema } from '~/lib/actions'
|
|
18
|
+
import { adminActionClient } from '~/lib/safe-action'
|
|
19
|
+
|
|
20
|
+
export const getUsers = adminActionClient
|
|
21
|
+
.inputSchema(
|
|
22
|
+
z.object({
|
|
23
|
+
...getPaginationSchema(),
|
|
24
|
+
...getSortSchema(['name', 'email', 'createdAt']),
|
|
25
|
+
search: z.string().trim().optional(),
|
|
26
|
+
filters: z
|
|
27
|
+
.object({
|
|
28
|
+
role: z.array(z.string()).optional(),
|
|
29
|
+
banned: z.array(z.boolean()).optional()
|
|
30
|
+
})
|
|
31
|
+
.optional()
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
.action(
|
|
35
|
+
async ({
|
|
36
|
+
parsedInput: { page, limit, sortBy, sortOrder, search, filters }
|
|
37
|
+
}) => {
|
|
38
|
+
const where = and(
|
|
39
|
+
search
|
|
40
|
+
? or(
|
|
41
|
+
ilike(usersTable.name, `%${search}%`),
|
|
42
|
+
ilike(usersTable.email, `%${search}%`)
|
|
43
|
+
)
|
|
44
|
+
: undefined,
|
|
45
|
+
filters?.role && filters.role.length > 0
|
|
46
|
+
? inArray(usersTable.role, filters.role)
|
|
47
|
+
: undefined,
|
|
48
|
+
filters?.banned && filters.banned.length > 0
|
|
49
|
+
? inArray(usersTable.banned, filters.banned)
|
|
50
|
+
: undefined
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const [results, total] = await Promise.all([
|
|
54
|
+
db.query.usersTable.findMany({
|
|
55
|
+
where,
|
|
56
|
+
limit,
|
|
57
|
+
offset: (page - 1) * limit,
|
|
58
|
+
orderBy: [
|
|
59
|
+
sortOrder === SortOrderEnum.ASC
|
|
60
|
+
? asc(usersTable[sortBy || 'createdAt'])
|
|
61
|
+
: desc(usersTable[sortBy || 'createdAt'])
|
|
62
|
+
]
|
|
63
|
+
}),
|
|
64
|
+
db.select({ count: count() }).from(usersTable).where(where)
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
return [results, total[0]?.count ?? 0] as const
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
export const getDistinctRoles = adminActionClient
|
|
72
|
+
.inputSchema(
|
|
73
|
+
z.object({
|
|
74
|
+
search: z.string().trim().optional(),
|
|
75
|
+
...getPaginationSchema(20)
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
.action(async ({ parsedInput: { search, limit } }) => {
|
|
79
|
+
const where = and(
|
|
80
|
+
isNotNull(usersTable.role),
|
|
81
|
+
search ? ilike(usersTable.role, `%${search}%`) : undefined
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const rows = await db
|
|
85
|
+
.selectDistinct({ role: usersTable.role })
|
|
86
|
+
.from(usersTable)
|
|
87
|
+
.where(where)
|
|
88
|
+
.orderBy(asc(usersTable.role))
|
|
89
|
+
.limit(limit)
|
|
90
|
+
|
|
91
|
+
return rows
|
|
92
|
+
.filter(r => r.role !== null)
|
|
93
|
+
.map(r => ({
|
|
94
|
+
value: r.role!,
|
|
95
|
+
label: r.role!.charAt(0).toUpperCase() + r.role!.slice(1)
|
|
96
|
+
}))
|
|
97
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { FC } from 'react'
|
|
4
|
+
import { DashboardHeader } from '~/components/dashboard-header'
|
|
5
|
+
import { AdminSearch } from './admin-search'
|
|
6
|
+
|
|
7
|
+
export const AdminHeader: FC<{
|
|
8
|
+
user: {
|
|
9
|
+
name?: string | null
|
|
10
|
+
email?: string | null
|
|
11
|
+
image?: string | null
|
|
12
|
+
}
|
|
13
|
+
isSuperAdmin: boolean
|
|
14
|
+
}> = ({ user, isSuperAdmin }) => {
|
|
15
|
+
return (
|
|
16
|
+
<DashboardHeader
|
|
17
|
+
user={user}
|
|
18
|
+
search={<AdminSearch isSuperAdmin={isSuperAdmin} />}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { FC } from 'react'
|
|
4
|
+
import { NavigationSearch } from '~/components/navigation-search'
|
|
5
|
+
import { getAdminNavigation } from '~/lib/admin-navigation'
|
|
6
|
+
|
|
7
|
+
export const AdminSearch: FC<{ isSuperAdmin: boolean }> = ({
|
|
8
|
+
isSuperAdmin
|
|
9
|
+
}) => {
|
|
10
|
+
return (
|
|
11
|
+
<NavigationSearch
|
|
12
|
+
groups={getAdminNavigation(isSuperAdmin)}
|
|
13
|
+
dialogDescription="Search across all admin pages and settings."
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { LayoutGrid } from 'lucide-react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { FC } from 'react'
|
|
6
|
+
import { NavigationSidebar } from '~/components/navigation-sidebar'
|
|
7
|
+
import {
|
|
8
|
+
SidebarMenu,
|
|
9
|
+
SidebarMenuButton,
|
|
10
|
+
SidebarMenuItem
|
|
11
|
+
} from '~/components/ui/sidebar'
|
|
12
|
+
import { getAdminNavigation } from '~/lib/admin-navigation'
|
|
13
|
+
|
|
14
|
+
export const AdminSidebar: FC<{
|
|
15
|
+
companyName: string
|
|
16
|
+
isSuperAdmin: boolean
|
|
17
|
+
}> = ({ companyName, isSuperAdmin }) => {
|
|
18
|
+
return (
|
|
19
|
+
<NavigationSidebar
|
|
20
|
+
groups={getAdminNavigation(isSuperAdmin)}
|
|
21
|
+
header={
|
|
22
|
+
<SidebarMenu>
|
|
23
|
+
<SidebarMenuItem>
|
|
24
|
+
<SidebarMenuButton size="lg" asChild>
|
|
25
|
+
<Link href="/admin">
|
|
26
|
+
<div className="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
|
|
27
|
+
<LayoutGrid className="size-4" />
|
|
28
|
+
</div>
|
|
29
|
+
<div className="flex min-w-0 flex-col gap-0.5 leading-none group-data-[collapsible=icon]:hidden">
|
|
30
|
+
<span className="truncate font-semibold">Admin Panel</span>
|
|
31
|
+
<span className="truncate text-xs">{companyName}</span>
|
|
32
|
+
</div>
|
|
33
|
+
</Link>
|
|
34
|
+
</SidebarMenuButton>
|
|
35
|
+
</SidebarMenuItem>
|
|
36
|
+
</SidebarMenu>
|
|
37
|
+
}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|