create-vexcms 0.0.3

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 (116) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +2409 -0
  3. package/package.json +37 -0
  4. package/templates/base-nextjs/.prettierignore +26 -0
  5. package/templates/base-nextjs/.prettierrc +7 -0
  6. package/templates/base-nextjs/README.md +256 -0
  7. package/templates/base-nextjs/_gitignore +2 -0
  8. package/templates/base-nextjs/components.json +24 -0
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +125 -0
  10. package/templates/base-nextjs/convex/_generated/api.js +23 -0
  11. package/templates/base-nextjs/convex/_generated/dataModel.d.ts +60 -0
  12. package/templates/base-nextjs/convex/_generated/server.d.ts +143 -0
  13. package/templates/base-nextjs/convex/_generated/server.js +93 -0
  14. package/templates/base-nextjs/convex/auth/adapter/index.ts +230 -0
  15. package/templates/base-nextjs/convex/auth/adapter/utils.ts +547 -0
  16. package/templates/base-nextjs/convex/auth/api.ts +8 -0
  17. package/templates/base-nextjs/convex/auth/config.ts +10 -0
  18. package/templates/base-nextjs/convex/auth/db.ts +303 -0
  19. package/templates/base-nextjs/convex/auth/index.ts +14 -0
  20. package/templates/base-nextjs/convex/auth/options.ts +63 -0
  21. package/templates/base-nextjs/convex/auth/plugins/index.ts +20 -0
  22. package/templates/base-nextjs/convex/auth/sessions.ts +60 -0
  23. package/templates/base-nextjs/convex/auth.config.ts +7 -0
  24. package/templates/base-nextjs/convex/convex.config.ts +5 -0
  25. package/templates/base-nextjs/convex/http.ts +28 -0
  26. package/templates/base-nextjs/convex/schema.ts +3 -0
  27. package/templates/base-nextjs/convex/vex/auth.ts +38 -0
  28. package/templates/base-nextjs/convex/vex/collections.ts +321 -0
  29. package/templates/base-nextjs/convex/vex/firstUser.ts +134 -0
  30. package/templates/base-nextjs/convex/vex/helpers.ts +33 -0
  31. package/templates/base-nextjs/convex/vex/impersonation.ts +51 -0
  32. package/templates/base-nextjs/convex/vex/media.ts +246 -0
  33. package/templates/base-nextjs/convex/vex/migrate.ts +74 -0
  34. package/templates/base-nextjs/convex/vex/model/collections.ts +196 -0
  35. package/templates/base-nextjs/convex/vex/model/media.ts +87 -0
  36. package/templates/base-nextjs/convex/vex/model/versions.ts +254 -0
  37. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +53 -0
  38. package/templates/base-nextjs/convex/vex/versions.ts +588 -0
  39. package/templates/base-nextjs/eslint.config.mjs +164 -0
  40. package/templates/base-nextjs/next.config.ts +10 -0
  41. package/templates/base-nextjs/package.json +67 -0
  42. package/templates/base-nextjs/postcss.config.mjs +7 -0
  43. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  44. package/templates/base-nextjs/public/file.svg +1 -0
  45. package/templates/base-nextjs/public/globe.svg +1 -0
  46. package/templates/base-nextjs/public/next.svg +1 -0
  47. package/templates/base-nextjs/public/vercel.svg +1 -0
  48. package/templates/base-nextjs/public/window.svg +1 -0
  49. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/page.tsx +9 -0
  50. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +23 -0
  51. package/templates/base-nextjs/src/app/(frontend)/@auth/default.tsx +3 -0
  52. package/templates/base-nextjs/src/app/(frontend)/@auth/page.tsx +3 -0
  53. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/page.tsx +9 -0
  54. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +11 -0
  55. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +14 -0
  56. package/templates/base-nextjs/src/app/(frontend)/page.tsx +116 -0
  57. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +33 -0
  58. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +38 -0
  59. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +101 -0
  60. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +15 -0
  61. package/templates/base-nextjs/src/app/admin/layout.tsx +33 -0
  62. package/templates/base-nextjs/src/app/api/auth/[...all]/route.ts +6 -0
  63. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  64. package/templates/base-nextjs/src/app/globals.css +130 -0
  65. package/templates/base-nextjs/src/app/layout.tsx +30 -0
  66. package/templates/base-nextjs/src/auth/client.tsx +41 -0
  67. package/templates/base-nextjs/src/auth/permissions.ts +102 -0
  68. package/templates/base-nextjs/src/auth/server.ts +16 -0
  69. package/templates/base-nextjs/src/auth/serverUtils.ts +64 -0
  70. package/templates/base-nextjs/src/auth/types.ts +3 -0
  71. package/templates/base-nextjs/src/components/component-example.tsx +474 -0
  72. package/templates/base-nextjs/src/components/example.tsx +52 -0
  73. package/templates/base-nextjs/src/components/providers/client.tsx +9 -0
  74. package/templates/base-nextjs/src/components/providers/convex.tsx +32 -0
  75. package/templates/base-nextjs/src/components/providers/server.tsx +15 -0
  76. package/templates/base-nextjs/src/components/providers/theme.tsx +11 -0
  77. package/templates/base-nextjs/src/components/ui/alert-dialog.tsx +162 -0
  78. package/templates/base-nextjs/src/components/ui/badge.tsx +52 -0
  79. package/templates/base-nextjs/src/components/ui/button.tsx +60 -0
  80. package/templates/base-nextjs/src/components/ui/card.tsx +92 -0
  81. package/templates/base-nextjs/src/components/ui/combobox.tsx +271 -0
  82. package/templates/base-nextjs/src/components/ui/dialog.tsx +135 -0
  83. package/templates/base-nextjs/src/components/ui/dropdown-menu.tsx +246 -0
  84. package/templates/base-nextjs/src/components/ui/field.tsx +224 -0
  85. package/templates/base-nextjs/src/components/ui/input-group.tsx +146 -0
  86. package/templates/base-nextjs/src/components/ui/input.tsx +20 -0
  87. package/templates/base-nextjs/src/components/ui/label.tsx +20 -0
  88. package/templates/base-nextjs/src/components/ui/select.tsx +189 -0
  89. package/templates/base-nextjs/src/components/ui/separator.tsx +21 -0
  90. package/templates/base-nextjs/src/components/ui/textarea.tsx +18 -0
  91. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +67 -0
  92. package/templates/base-nextjs/src/db/constants/auth.ts +8 -0
  93. package/templates/base-nextjs/src/db/constants/index.ts +44 -0
  94. package/templates/base-nextjs/src/db/types.ts +6 -0
  95. package/templates/base-nextjs/src/env.mjs +48 -0
  96. package/templates/base-nextjs/src/lib/utils.ts +6 -0
  97. package/templates/base-nextjs/src/proxy.ts +23 -0
  98. package/templates/base-nextjs/src/vexcms/access.ts +28 -0
  99. package/templates/base-nextjs/src/vexcms/auth.ts +4 -0
  100. package/templates/base-nextjs/src/vexcms/collections/index.ts +1 -0
  101. package/templates/base-nextjs/src/vexcms/collections/media.ts +15 -0
  102. package/templates/base-nextjs/src/vexcms/collections/users.ts +46 -0
  103. package/templates/base-nextjs/tsconfig.json +60 -0
  104. package/templates/base-nextjs/vex.config.ts +23 -0
  105. package/templates/marketing-site/convex/pages.ts +46 -0
  106. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +58 -0
  107. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +69 -0
  108. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +71 -0
  109. package/templates/marketing-site/src/db/constants/index.ts +52 -0
  110. package/templates/marketing-site/src/vexcms/collections/footers.ts +28 -0
  111. package/templates/marketing-site/src/vexcms/collections/headers.ts +35 -0
  112. package/templates/marketing-site/src/vexcms/collections/index.ts +7 -0
  113. package/templates/marketing-site/src/vexcms/collections/pages.ts +40 -0
  114. package/templates/marketing-site/src/vexcms/collections/site_settings.ts +31 -0
  115. package/templates/marketing-site/src/vexcms/collections/themes.ts +37 -0
  116. package/templates/marketing-site/vex.config.ts +32 -0
@@ -0,0 +1,164 @@
1
+ import convexPlugin from "@convex-dev/eslint-plugin"
2
+ import { FlatCompat } from "@eslint/eslintrc"
3
+ import importX from "eslint-plugin-import-x"
4
+ import perfectionist from "eslint-plugin-perfectionist"
5
+ import tseslint from "typescript-eslint"
6
+
7
+ const compat = new FlatCompat({
8
+ baseDirectory: import.meta.dirname,
9
+ })
10
+
11
+ const convexConfig = convexPlugin.configs.recommended[0]
12
+ const convexRules = convexConfig.rules
13
+
14
+ export const defaultESLintIgnores = [
15
+ "**/.temp",
16
+ "**/.git",
17
+ "**/.hg",
18
+ "**/.pnp.*",
19
+ "**/.svn",
20
+ "**/.next",
21
+ "**/.open-next",
22
+ "**/.sst",
23
+ "**/playwright.config.ts",
24
+ "**/vitest.config.ts",
25
+ "**/tsconfig.tsbuildinfo",
26
+ "**/README.md",
27
+ "**/eslint.config.js",
28
+ "**/dist/",
29
+ "**/.yarn/",
30
+ "**/build/",
31
+ "**/node_modules/",
32
+ "**/temp/",
33
+ "**/registry/",
34
+ "**/public/",
35
+ "**/convex/_generated/",
36
+ "**/.source/",
37
+ "**/sst.config.ts",
38
+ "**/sst.workflow.ts",
39
+ ]
40
+
41
+ export default tseslint.config(
42
+ {
43
+ ignores: defaultESLintIgnores,
44
+ },
45
+ ...compat.extends("next/core-web-vitals"),
46
+ perfectionist.configs["recommended-natural"],
47
+ {
48
+ extends: [
49
+ ...tseslint.configs.recommended,
50
+ ...tseslint.configs.recommendedTypeChecked,
51
+ ...tseslint.configs.stylisticTypeChecked,
52
+ ],
53
+ files: ["**/*.ts", "**/*.tsx"],
54
+ plugins: {
55
+ "import-x": importX,
56
+ },
57
+ rules: {
58
+ "@typescript-eslint/array-type": "off",
59
+ "@typescript-eslint/ban-tslint-comment": "off",
60
+ "@typescript-eslint/consistent-type-definitions": "off",
61
+ "@typescript-eslint/consistent-type-imports": [
62
+ "warn",
63
+ { fixStyle: "inline-type-imports", prefer: "type-imports" },
64
+ ],
65
+ "@typescript-eslint/no-misused-promises": [
66
+ "error",
67
+ { checksVoidReturn: { attributes: false } },
68
+ ],
69
+ "@typescript-eslint/no-unused-vars": [
70
+ "warn",
71
+ {
72
+ argsIgnorePattern: "^_",
73
+ caughtErrorsIgnorePattern: "^(_|ignore)",
74
+ destructuredArrayIgnorePattern: "^_",
75
+ varsIgnorePattern: "^_",
76
+ },
77
+ ],
78
+ "@typescript-eslint/require-await": "off",
79
+ "@typescript-eslint/unbound-method": "off",
80
+
81
+ curly: ["warn", "all"],
82
+ "import-x/no-duplicates": "warn",
83
+ "import-x/prefer-default-export": "off",
84
+ "no-console": "off",
85
+ // Allow default exports for Next.js
86
+ "no-restricted-exports": "off",
87
+ "no-underscore-dangle": "off",
88
+ "no-useless-escape": "warn",
89
+
90
+ "object-shorthand": "warn",
91
+
92
+ "perfectionist/sort-imports": "warn",
93
+ "perfectionist/sort-interfaces": "warn",
94
+ "perfectionist/sort-intersection-types": "warn",
95
+ // Perfectionist sorting configuration (from PayloadCMS)
96
+ "perfectionist/sort-jsx-props": "warn",
97
+ "perfectionist/sort-modules": "warn",
98
+ "perfectionist/sort-named-imports": "warn",
99
+ "perfectionist/sort-object-types": "warn",
100
+ "perfectionist/sort-objects": [
101
+ "warn",
102
+ {
103
+ customGroups: [
104
+ {
105
+ elementNamePattern: "^(_id|id|name|slug|type)$",
106
+ groupName: "top",
107
+ },
108
+ ],
109
+ groups: ["top", "unknown"],
110
+ order: "asc",
111
+ partitionByComment: true,
112
+ partitionByNewLine: true,
113
+ type: "natural",
114
+ },
115
+ ],
116
+ "perfectionist/sort-switch-case": "off",
117
+ "perfectionist/sort-union-types": "warn",
118
+ },
119
+ },
120
+ {
121
+ // Disable rules for test files and config files
122
+ files: ["src/test/**/*.js", "src/test/**/*.ts", "tests/**/*.ts", "*.config.js", "*.config.mjs"],
123
+ rules: {
124
+ "import/no-anonymous-default-export": "off",
125
+ },
126
+ },
127
+ {
128
+ // Disable object sorting in VEX collection/config files — field order determines
129
+ // the order of fields in admin panel forms
130
+ files: [
131
+ "**/vexcms/collections/**/*.ts",
132
+ "**/vexcms/access.*",
133
+ "**/vex.config.*",
134
+ "**/db/constants/**/*.ts",
135
+ ],
136
+ rules: {
137
+ "perfectionist/sort-objects": "off",
138
+ },
139
+ },
140
+ {
141
+ files: ["**/src/convex/**/*.ts"],
142
+ plugins: {
143
+ "@convex-dev": convexPlugin,
144
+ },
145
+ rules: convexRules,
146
+ },
147
+ {
148
+ languageOptions: {
149
+ parserOptions: {
150
+ ecmaVersion: "latest",
151
+ project: "./tsconfig.json",
152
+ sourceType: "module",
153
+ tsconfigRootDir: import.meta.dirname,
154
+ },
155
+ },
156
+ linterOptions: {
157
+ reportUnusedDisableDirectives: "error",
158
+ },
159
+ settings: {
160
+ // Workaround for ESLint flat config circular reference in diagnostics
161
+ _internalSilentUse: true,
162
+ },
163
+ }
164
+ )
@@ -0,0 +1,10 @@
1
+ import type { NextConfig } from "next"
2
+
3
+ import "./src/env.mjs"
4
+
5
+ const nextConfig: NextConfig = {
6
+ allowedDevOrigins: ["127.0.01", "localhost"],
7
+ reactCompiler: true,
8
+ }
9
+
10
+ export default nextConfig
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --port=3010",
7
+ "vex:dev": "vex dev",
8
+ "vex:generate": "vex dev --once",
9
+ "vex:update": "pnpm add @vexcms/core@latest @vexcms/cli@latest @vexcms/admin-next@latest @vexcms/ui@latest @vexcms/richtext@latest @vexcms/better-auth@latest @vexcms/file-storage-convex@latest",
10
+ "build": "next build",
11
+ "start": "next start",
12
+ "lint": "eslint .",
13
+ "format": "prettier --write .",
14
+ "typecheck": "tsc --noEmit",
15
+ "secret:create": "openssl rand -base64 32 | tr -d '\\n' | tee /dev/stderr | pbcopy && echo '\\n✓ Secret copied to clipboard'"
16
+ },
17
+ "dependencies": {
18
+ "@base-ui/react": "^1.1.0",
19
+ "@convex-dev/better-auth": "^0.10.10",
20
+ "@convex-dev/react-query": "^0.1.0",
21
+ "@daveyplate/better-auth-ui": "^3.3.15",
22
+ "@t3-oss/env-nextjs": "^0.13.10",
23
+ "@tanstack/react-form": "^1.27.7",
24
+ "@tanstack/react-query": "^5.90.17",
25
+ "@vexcms/admin-next": "^0.0.2",
26
+ "@vexcms/better-auth": "^0.0.2",
27
+ "@vexcms/core": "^0.0.2",
28
+ "@vexcms/richtext": "^0.0.2",
29
+ "@vexcms/ui": "^0.0.2",
30
+ "@vexcms/file-storage-convex": "^0.0.2",
31
+ "better-auth": ">=1.4.9 <1.5.0",
32
+ "class-variance-authority": "^0.7.1",
33
+ "clsx": "^2.1.1",
34
+ "convex": "^1.31.5",
35
+ "convex-helpers": "^0.1.111",
36
+ "lucide-react": "^0.562.0",
37
+ "next": "^16.1.0",
38
+ "next-themes": "^0.4.6",
39
+ "nuqs": "^2.8.8",
40
+ "react": "^19.2.4",
41
+ "react-dom": "^19.2.4",
42
+ "shadcn": "^3.6.3",
43
+ "tailwind-merge": "^3.4.0",
44
+ "tw-animate-css": "^1.4.0",
45
+ "zod": "^4.3.5"
46
+ },
47
+ "devDependencies": {
48
+ "@convex-dev/eslint-plugin": "^1.1.1",
49
+ "@eslint/eslintrc": "^3.3.3",
50
+ "@next/eslint-plugin-next": "^15.4.3",
51
+ "@tailwindcss/postcss": "^4",
52
+ "@types/node": "^20",
53
+ "@types/react": "^19",
54
+ "@types/react-dom": "^19",
55
+ "@vexcms/cli": "^0.0.2",
56
+ "babel-plugin-react-compiler": "^1.0.0",
57
+ "eslint": "^9",
58
+ "eslint-config-next": "15.5.9",
59
+ "eslint-plugin-import-x": "^4.16.1",
60
+ "eslint-plugin-perfectionist": "^5.3.1",
61
+ "eslint-plugin-react-hooks": "^7.0.1",
62
+ "prettier": "^3.8.1",
63
+ "tailwindcss": "^4",
64
+ "typescript": "^5.9.3",
65
+ "typescript-eslint": "^8.53.0"
66
+ }
67
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ }
6
+
7
+ export default config
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,9 @@
1
+ import AuthView from "./view"
2
+
3
+ export const dynamic = "force-dynamic"
4
+
5
+ export default async function AuthPage({ params }: { params: Promise<{ pathname: string }> }) {
6
+ const { pathname } = await params
7
+
8
+ return <AuthView pathname={pathname} />
9
+ }
@@ -0,0 +1,23 @@
1
+ import { AuthView } from "@daveyplate/better-auth-ui"
2
+ import { Activity } from "react"
3
+
4
+ import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog"
5
+
6
+ export default function AuthCard({ pathname }: { pathname: string }) {
7
+ return (
8
+ <main>
9
+ <Dialog open>
10
+ <Activity mode="hidden">
11
+ <DialogTitle />
12
+ </Activity>
13
+ <DialogContent
14
+ aria-describedby={undefined}
15
+ className="grid place-items-center border-none bg-transparent shadow-none ring-0"
16
+ showCloseButton={false}
17
+ >
18
+ <AuthView path={pathname} />
19
+ </DialogContent>
20
+ </Dialog>
21
+ </main>
22
+ )
23
+ }
@@ -0,0 +1,3 @@
1
+ export default function Page() {
2
+ return null
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function Page() {
2
+ return null
3
+ }
@@ -0,0 +1,9 @@
1
+ import AuthView from "./view"
2
+
3
+ export const dynamic = "force-dynamic"
4
+
5
+ export default async function AuthPage({ params }: { params: Promise<{ pathname: string }> }) {
6
+ const { pathname } = await params
7
+
8
+ return <AuthView pathname={pathname} />
9
+ }
@@ -0,0 +1,11 @@
1
+ "use client"
2
+
3
+ import { AuthView } from "@daveyplate/better-auth-ui"
4
+
5
+ export default function AuthCard({ pathname }: { pathname: string }) {
6
+ return (
7
+ <main className="absolute inset-0 grid place-items-center">
8
+ <AuthView path={pathname} />
9
+ </main>
10
+ )
11
+ }
@@ -0,0 +1,14 @@
1
+ export default function FrontendLayout({
2
+ auth,
3
+ children,
4
+ }: Readonly<{
5
+ auth: React.ReactNode
6
+ children: React.ReactNode
7
+ }>) {
8
+ return (
9
+ <>
10
+ {children}
11
+ {auth}
12
+ </>
13
+ )
14
+ }
@@ -0,0 +1,116 @@
1
+ "use client"
2
+
3
+ import { useMutation, useQuery } from "convex/react"
4
+ import { usePathname, useRouter } from "next/navigation"
5
+ import { useEffect, useState } from "react"
6
+
7
+ import { api } from "@convex/_generated/api"
8
+ import { useSession } from "~/auth/client"
9
+
10
+ function LoadingSpinner() {
11
+ return (
12
+ <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
13
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
14
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
15
+ </svg>
16
+ )
17
+ }
18
+
19
+ export default function Page() {
20
+ const isBootstrapped = useQuery(api.vex.firstUser.isBootstrapped)
21
+ const promoteFirstAdmin = useMutation(api.vex.firstUser.promoteFirstAdmin)
22
+ const { data: session } = useSession()
23
+ const router = useRouter()
24
+ const pathname = usePathname()
25
+ const [promoting, setPromoting] = useState(false)
26
+ const [navigating, setNavigating] = useState(false)
27
+
28
+ // Reset navigating state when the user comes back to the home page
29
+ // (e.g. closing the intercepting auth dialog)
30
+ useEffect(() => {
31
+ if (pathname === "/") {
32
+ setNavigating(false)
33
+ }
34
+ }, [pathname])
35
+
36
+ // If user is signed in and this is a fresh project, try to promote them
37
+ useEffect(() => {
38
+ if (session?.user && isBootstrapped === false && !promoting) {
39
+ setPromoting(true)
40
+ setNavigating(true)
41
+ promoteFirstAdmin()
42
+ .then((result) => {
43
+ if (result.promoted) {
44
+ router.push("/admin")
45
+ } else {
46
+ setNavigating(false)
47
+ }
48
+ })
49
+ .catch(() => {
50
+ setPromoting(false)
51
+ setNavigating(false)
52
+ })
53
+ }
54
+ }, [session, isBootstrapped, promoting, promoteFirstAdmin, router])
55
+
56
+ // Treat undefined (query still loading) as not bootstrapped — show the welcome page immediately
57
+ const bootstrapped = isBootstrapped === true
58
+
59
+ const buttonClass =
60
+ "inline-flex items-center justify-center gap-2 rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
61
+
62
+ const handleNavigate = (path: string) => {
63
+ setNavigating(true)
64
+ router.push(path)
65
+ }
66
+
67
+ return (
68
+ <div className="flex min-h-screen flex-col items-center justify-center gap-8 p-8">
69
+ <div className="text-center">
70
+ <h1 className="text-4xl font-bold tracking-tight">VEX CMS</h1>
71
+ <p className="mt-3 text-lg text-muted-foreground">
72
+ {bootstrapped
73
+ ? "Your content management system is ready."
74
+ : "Welcome! Create your admin account to get started."}
75
+ </p>
76
+ </div>
77
+
78
+ <div className="flex gap-4">
79
+ {session?.user ? (
80
+ <button
81
+ onClick={() => handleNavigate("/admin")}
82
+ disabled={navigating}
83
+ className={buttonClass}
84
+ >
85
+ {navigating && <LoadingSpinner />}
86
+ {navigating ? "Loading..." : "Go to Admin Panel"}
87
+ </button>
88
+ ) : bootstrapped ? (
89
+ <button
90
+ onClick={() => handleNavigate("/auth/sign-in")}
91
+ disabled={navigating}
92
+ className={buttonClass}
93
+ >
94
+ {navigating && <LoadingSpinner />}
95
+ {navigating ? "Loading..." : "Sign In"}
96
+ </button>
97
+ ) : (
98
+ <button
99
+ onClick={() => handleNavigate("/auth/sign-up")}
100
+ disabled={navigating}
101
+ className={buttonClass}
102
+ >
103
+ {navigating && <LoadingSpinner />}
104
+ {navigating ? "Loading..." : "Create Admin Account"}
105
+ </button>
106
+ )}
107
+ </div>
108
+
109
+ {promoting && (
110
+ <p className="text-sm text-muted-foreground animate-pulse">
111
+ Setting up your admin account...
112
+ </p>
113
+ )}
114
+ </div>
115
+ )
116
+ }
@@ -0,0 +1,33 @@
1
+ "use client"
2
+
3
+ import { AdminLayout } from "@vexcms/admin-next"
4
+ import type { PermissionUser } from "@vexcms/admin-next"
5
+ import type { ClientVexConfig } from "@vexcms/core"
6
+ import { access } from "~/vexcms/access"
7
+
8
+ /**
9
+ * Client wrapper that provides the access config directly (not through RSC serialization).
10
+ * The access config contains callback functions that can't be serialized through RSC.
11
+ */
12
+ export function AdminLayoutWrapper({
13
+ config,
14
+ user,
15
+ permissionUser,
16
+ children,
17
+ }: {
18
+ config: ClientVexConfig
19
+ user?: { name: string; email: string; avatar?: string }
20
+ permissionUser?: PermissionUser
21
+ children: React.ReactNode
22
+ }) {
23
+ return (
24
+ <AdminLayout
25
+ config={config}
26
+ user={user}
27
+ permissionUser={permissionUser}
28
+ access={access}
29
+ >
30
+ {children}
31
+ </AdminLayout>
32
+ )
33
+ }
@@ -0,0 +1,38 @@
1
+ "use client"
2
+
3
+ import { AdminPage } from "@vexcms/admin-next"
4
+ import type { ClientVexConfig } from "@vexcms/core"
5
+ import { extractLivePreviewConfigs, sanitizeConfigForClient } from "@vexcms/core"
6
+ import { RichTextFieldWithMedia } from "./RichTextFieldWithMedia"
7
+ import originalConfig from "~/../vex.config"
8
+
9
+ const livePreviewConfigs = extractLivePreviewConfigs(originalConfig)
10
+ // Sanitize on the client so component references in admin.components survive
11
+ // (they can't cross the RSC serialization boundary)
12
+ const clientConfig = sanitizeConfigForClient(originalConfig)
13
+
14
+ export function AdminPageWrapper({
15
+ config: _serverConfig,
16
+ path,
17
+ }: {
18
+ config: ClientVexConfig
19
+ path?: string[]
20
+ }) {
21
+ return (
22
+ <AdminPage
23
+ config={clientConfig}
24
+ path={path}
25
+ livePreviewConfigs={livePreviewConfigs}
26
+ renderRichTextField={(props: any) => (
27
+ <RichTextFieldWithMedia
28
+ field={props.field}
29
+ fieldDef={props.fieldDef}
30
+ name={props.name}
31
+ generateUploadUrl={props.generateUploadUrl}
32
+ createMediaDocument={props.createMediaDocument}
33
+ onUploadNew={props.onUploadNew}
34
+ />
35
+ )}
36
+ />
37
+ )
38
+ }
@@ -0,0 +1,101 @@
1
+ "use client"
2
+
3
+ import { PlateEditorField } from "@vexcms/richtext/editor"
4
+ import { defaultFeatures } from "@vexcms/richtext/editor"
5
+ import { useMediaPickerState } from "@vexcms/admin-next"
6
+
7
+ /**
8
+ * Inner component that always calls useMediaPickerState (no conditional hook).
9
+ */
10
+ function WithMedia({
11
+ field,
12
+ fieldDef,
13
+ name,
14
+ mediaCollection,
15
+ generateUploadUrl,
16
+ createMediaDocument,
17
+ onUploadNew,
18
+ }: {
19
+ field: any
20
+ fieldDef: any
21
+ name: string
22
+ mediaCollection: string
23
+ generateUploadUrl?: () => Promise<string>
24
+ createMediaDocument?: (props: { collectionSlug: string; fields: Record<string, unknown> }) => Promise<string>
25
+ onUploadNew?: () => void
26
+ }) {
27
+ const mediaPicker = useMediaPickerState({
28
+ collectionSlug: mediaCollection,
29
+ searchField: "filename",
30
+ searchIndexName: "search_filename",
31
+ selectedId: null,
32
+ })
33
+
34
+ return (
35
+ <PlateEditorField
36
+ value={field.state.value}
37
+ onChange={(val: any) => field.handleChange(val)}
38
+ name={name}
39
+ label={fieldDef.label ?? name}
40
+ description={fieldDef.description}
41
+ features={defaultFeatures}
42
+ mediaCollection={mediaCollection}
43
+ mediaResults={mediaPicker.results}
44
+ mediaSearchTerm={mediaPicker.searchTerm}
45
+ onMediaSearchChange={mediaPicker.setSearchTerm}
46
+ mediaCanLoadMore={mediaPicker.canLoadMore}
47
+ onMediaLoadMore={mediaPicker.loadMore}
48
+ mediaIsLoading={mediaPicker.isLoading}
49
+ onUploadNew={onUploadNew}
50
+ generateUploadUrl={generateUploadUrl}
51
+ createMediaDocument={createMediaDocument}
52
+ />
53
+ )
54
+ }
55
+
56
+ /**
57
+ * Wrapper that creates media picker state for a richtext field
58
+ * when mediaCollection is configured.
59
+ */
60
+ export function RichTextFieldWithMedia({
61
+ field,
62
+ fieldDef,
63
+ name,
64
+ generateUploadUrl,
65
+ createMediaDocument,
66
+ onUploadNew,
67
+ }: {
68
+ field: any
69
+ fieldDef: any
70
+ name: string
71
+ generateUploadUrl?: () => Promise<string>
72
+ createMediaDocument?: (props: { collectionSlug: string; fields: Record<string, unknown> }) => Promise<string>
73
+ onUploadNew?: () => void
74
+ }) {
75
+ const mediaCollection = fieldDef?.mediaCollection as string | undefined
76
+
77
+ if (mediaCollection) {
78
+ return (
79
+ <WithMedia
80
+ field={field}
81
+ fieldDef={fieldDef}
82
+ name={name}
83
+ mediaCollection={mediaCollection}
84
+ generateUploadUrl={generateUploadUrl}
85
+ createMediaDocument={createMediaDocument}
86
+ onUploadNew={onUploadNew}
87
+ />
88
+ )
89
+ }
90
+
91
+ return (
92
+ <PlateEditorField
93
+ value={field.state.value}
94
+ onChange={(val: any) => field.handleChange(val)}
95
+ name={name}
96
+ label={fieldDef.label ?? name}
97
+ description={fieldDef.description}
98
+ features={defaultFeatures}
99
+ />
100
+ )
101
+ }
@@ -0,0 +1,15 @@
1
+ import { sanitizeConfigForClient } from "@vexcms/core"
2
+
3
+ import config from "~/../vex.config"
4
+ import { AdminPageWrapper } from "../AdminPageWrapper"
5
+
6
+ const clientConfig = sanitizeConfigForClient(config)
7
+
8
+ interface Props {
9
+ params: Promise<{ path?: string[] }>
10
+ }
11
+
12
+ export default async function Page({ params }: Props) {
13
+ const { path } = await params
14
+ return <AdminPageWrapper config={clientConfig} path={path} />
15
+ }