create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.21
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 +48 -14
- package/dist/index.js +1714 -1504
- package/package.json +2 -2
- package/templates/base-nextjs/README.md +18 -5
- package/templates/base-nextjs/convex/auth/index.ts +2 -2
- package/templates/base-nextjs/convex/auth/options.ts +27 -26
- package/templates/base-nextjs/convex/auth/schema.ts +51 -0
- package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
- package/templates/base-nextjs/convex/tsconfig.json +2 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
- package/templates/base-nextjs/convex/vex/globals.ts +3 -2
- package/templates/base-nextjs/convex/vex/media.ts +3 -2
- package/templates/base-nextjs/convex/vex.schema.ts +1 -0
- package/templates/base-nextjs/convex/vex.ts +17 -4
- package/templates/base-nextjs/next.config.ts +52 -6
- package/templates/base-nextjs/package.json +18 -7
- package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
- package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
- package/templates/base-nextjs/src/app/globals.css +10 -9
- package/templates/base-nextjs/src/app/layout.tsx +6 -0
- package/templates/base-nextjs/src/auth/access.ts +14 -2
- package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
- package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
- package/templates/base-nextjs/src/components/component-example.tsx +1 -1
- package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
- package/templates/base-nextjs/src/vex.config.server.ts +30 -0
- package/templates/base-nextjs/src/vex.config.ts +14 -11
- package/templates/base-nextjs/src/vex.types.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
- package/templates/marketing-site/convex/pages.ts +20 -1
- package/templates/marketing-site/convex/schema.ts +4 -4
- package/templates/marketing-site/convex/seed.ts +515 -70
- package/templates/marketing-site/convex/siteSettings.ts +1 -1
- package/templates/marketing-site/convex/theme.ts +3 -3
- package/templates/marketing-site/convex/vex.schema.ts +75 -38
- package/templates/marketing-site/public/favicon.svg +14 -0
- package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
- package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
- package/templates/marketing-site/src/app/globals.css +413 -0
- package/templates/marketing-site/src/app/layout.tsx +85 -0
- package/templates/marketing-site/src/app/robots.ts +13 -0
- package/templates/marketing-site/src/app/sitemap.ts +18 -0
- package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
- package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
- package/templates/marketing-site/src/components/CodePane.tsx +98 -0
- package/templates/marketing-site/src/components/Container.tsx +25 -0
- package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
- package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
- package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
- package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
- package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
- package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
- package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
- package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
- package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
- package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
- package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
- package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +47 -266
- package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
- package/templates/marketing-site/src/lib/highlight.ts +134 -0
- package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
- package/templates/marketing-site/src/lib/metadata.ts +61 -33
- package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
- package/templates/marketing-site/src/lib/vex.ts +18 -0
- package/templates/marketing-site/src/proxy.ts +179 -0
- package/templates/marketing-site/src/vex.config.server.ts +27 -0
- package/templates/marketing-site/src/vex.config.ts +27 -9
- package/templates/marketing-site/src/vex.types.ts +102 -76
- package/templates/marketing-site/src/vexcms/api.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
- package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
- package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
- package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
- package/dist/index.mjs +0 -30976
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
- package/templates/marketing-site/src/app/(frontend)/page.tsx +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-vexcms",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
4
4
|
"description": "Scaffold a new VexCMS project",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsup src/index.ts --format esm --dts",
|
|
54
|
-
"dev": "tsup src/index.ts --format esm --watch",
|
|
54
|
+
"dev": "tsup src/index.ts --format esm --watch src",
|
|
55
55
|
"test": "vitest run"
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -31,9 +31,21 @@ NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
|
|
|
31
31
|
NEXT_PUBLIC_CONVEX_SITE_URL=https://your-deployment.convex.site
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
`next.config.ts` reads `NEXT_PUBLIC_CONVEX_URL` at build/start time and adds that
|
|
35
|
+
deployment's hostname to `images.remotePatterns` automatically, so `next/image` can serve
|
|
36
|
+
uploaded media without a manual edit.
|
|
37
|
+
|
|
38
|
+
Better Auth runs inside your Convex deployment, not in the Next.js server —
|
|
39
|
+
it does **not** read `.env.local`. Set the same two values on the
|
|
40
|
+
deployment itself:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx convex env set SITE_URL http://localhost:3010
|
|
44
|
+
npx convex env set BETTER_AUTH_SECRET <the BETTER_AUTH_SECRET value from .env.local>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Skipping this step is the most common cause of a `403` on your first sign-in
|
|
48
|
+
attempt.
|
|
37
49
|
|
|
38
50
|
### 4. Run the dev servers
|
|
39
51
|
|
|
@@ -65,10 +77,11 @@ see `convex/vex/firstUser.ts`. From there, "Go to Admin Panel" opens `/admin`.
|
|
|
65
77
|
│ ├── auth/ # Auth client + server helpers, access control
|
|
66
78
|
│ ├── components/ # React components
|
|
67
79
|
│ ├── vexcms/collections/ # Collection definitions
|
|
68
|
-
│ ├── vex.config.ts # VexCMS config
|
|
80
|
+
│ ├── vex.config.ts # VexCMS client config (collections, globals, access)
|
|
81
|
+
│ ├── vex.config.server.ts # VexCMS server config (auth adapter, storage adapters)
|
|
69
82
|
│ └── env.mjs # Typed environment variables
|
|
70
83
|
├── convex/ # Convex backend
|
|
71
|
-
│ ├── auth/ # Better Auth config + plugins
|
|
84
|
+
│ ├── auth/ # Better Auth config + plugins (schema.ts, options.ts)
|
|
72
85
|
│ ├── vex/ # Bootstrap, media, globals endpoints
|
|
73
86
|
│ └── schema.ts # Database schema
|
|
74
87
|
└── .env.local # Environment variables (not committed)
|
|
@@ -3,10 +3,10 @@ import type { GenericActionCtx } from "convex/server"
|
|
|
3
3
|
import { createBetterAuthAdapter } from "@vexcms/better-auth"
|
|
4
4
|
import { betterAuth } from "better-auth"
|
|
5
5
|
|
|
6
|
-
import { authOptions } from "./options"
|
|
7
|
-
|
|
8
6
|
import type { DataModel } from "../_generated/dataModel"
|
|
9
7
|
|
|
8
|
+
import { authOptions } from "./options"
|
|
9
|
+
|
|
10
10
|
export const createAuth = (ctx: GenericActionCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
|
|
11
11
|
return betterAuth({
|
|
12
12
|
database: createBetterAuthAdapter(ctx),
|
|
@@ -1,39 +1,40 @@
|
|
|
1
1
|
import type { BetterAuthOptions } from "better-auth"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
TABLE_SLUG_USERS,
|
|
7
|
-
TABLE_SLUG_VERIFICATIONS,
|
|
8
|
-
USER_ROLES,
|
|
9
|
-
} from "~/db/constants"
|
|
3
|
+
import { anonRoleDatabaseHook } from "@vexcms/better-auth"
|
|
4
|
+
|
|
5
|
+
import { USER_ROLES } from "~/db/constants"
|
|
10
6
|
|
|
11
7
|
import { createPlugins } from "./plugins"
|
|
8
|
+
import { authSchema } from "./schema"
|
|
9
|
+
|
|
10
|
+
// `plugins` on the shared schema is a descriptor map for
|
|
11
|
+
// `betterAuthCollections`, not Better Auth plugin instances — dropped by name
|
|
12
|
+
// so the intent survives a reordering of the object literal below.
|
|
13
|
+
const { plugins: _pluginDescriptors, ...schemaOptions } = authSchema
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Better Auth options for this project.
|
|
17
|
+
*
|
|
18
|
+
* Model names and `user.additionalFields` come from `./schema`, the one module
|
|
19
|
+
* both this file and `src/vex.config.ts` read, so a field added there reaches
|
|
20
|
+
* the database and the admin panel without being written twice. Everything
|
|
21
|
+
* below the spread is server-only.
|
|
22
|
+
*
|
|
23
|
+
* @see ./schema for the shared, client-safe half
|
|
24
|
+
*/
|
|
13
25
|
export const authOptions: BetterAuthOptions = {
|
|
14
|
-
|
|
15
|
-
modelName: TABLE_SLUG_ACCOUNTS,
|
|
16
|
-
},
|
|
26
|
+
...schemaOptions,
|
|
17
27
|
baseURL: process.env.SITE_URL,
|
|
28
|
+
databaseHooks: {
|
|
29
|
+
// Ties Better Auth's anonymous-plugin users to `access.anonRole` (see
|
|
30
|
+
// `~/auth/access.ts`) by stamping the same role explicitly, rather than
|
|
31
|
+
// relying solely on `roles`' `defaultValue` below — which every new user
|
|
32
|
+
// gets regardless of `isAnonymous`, anon-plugin or not.
|
|
33
|
+
user: anonRoleDatabaseHook(USER_ROLES.user),
|
|
34
|
+
},
|
|
18
35
|
// {{EMAIL_PASSWORD_AUTH}}
|
|
19
36
|
// {{OAUTH_PROVIDERS}}
|
|
20
37
|
plugins: createPlugins(),
|
|
21
38
|
secret: process.env.BETTER_AUTH_SECRET,
|
|
22
|
-
session: {
|
|
23
|
-
modelName: TABLE_SLUG_SESSIONS,
|
|
24
|
-
},
|
|
25
39
|
trustedOrigins: [process.env.SITE_URL!],
|
|
26
|
-
user: {
|
|
27
|
-
additionalFields: {
|
|
28
|
-
roles: {
|
|
29
|
-
type: "string[]",
|
|
30
|
-
defaultValue: [USER_ROLES.user],
|
|
31
|
-
required: true,
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
modelName: TABLE_SLUG_USERS,
|
|
35
|
-
},
|
|
36
|
-
verification: {
|
|
37
|
-
modelName: TABLE_SLUG_VERIFICATIONS,
|
|
38
|
-
},
|
|
39
40
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BetterAuthSchemaInput } from "@vexcms/better-auth/client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
TABLE_SLUG_ACCOUNTS,
|
|
5
|
+
TABLE_SLUG_SESSIONS,
|
|
6
|
+
TABLE_SLUG_USERS,
|
|
7
|
+
TABLE_SLUG_VERIFICATIONS,
|
|
8
|
+
USER_ROLES,
|
|
9
|
+
} from "~/db/constants"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Everything about your Better Auth setup that determines which collections
|
|
13
|
+
* exist and which fields they carry — and nothing else.
|
|
14
|
+
*
|
|
15
|
+
* This is the one auth module the browser reads: `src/vex.config.ts` passes it
|
|
16
|
+
* to `betterAuthCollections()` to build the admin panel's auth collections,
|
|
17
|
+
* and `./options` spreads it so the same model names and fields reach the
|
|
18
|
+
* database. Add a `user.additionalFields` entry here and it shows up in both.
|
|
19
|
+
*
|
|
20
|
+
* Keep it inert. `./options` reads `process.env` and `./plugins` pulls
|
|
21
|
+
* `better-auth/next-js` (→ `next/headers`), which is why they are separate
|
|
22
|
+
* files — importing either from the client config would drag the server into
|
|
23
|
+
* your browser bundle. Runtime plugin options (admin roles, mailers, OAuth
|
|
24
|
+
* secrets) belong on the real plugin instances in `./plugins`; only options
|
|
25
|
+
* that change which TABLES exist belong here.
|
|
26
|
+
*
|
|
27
|
+
* @see betterAuthCollections in @vexcms/better-auth/client
|
|
28
|
+
* @see ./options for the server-side options that spread this
|
|
29
|
+
*/
|
|
30
|
+
export const authSchema = {
|
|
31
|
+
account: { modelName: TABLE_SLUG_ACCOUNTS },
|
|
32
|
+
session: { modelName: TABLE_SLUG_SESSIONS },
|
|
33
|
+
user: {
|
|
34
|
+
modelName: TABLE_SLUG_USERS,
|
|
35
|
+
additionalFields: {
|
|
36
|
+
roles: {
|
|
37
|
+
type: "string[]",
|
|
38
|
+
defaultValue: [USER_ROLES.user],
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
verification: { modelName: TABLE_SLUG_VERIFICATIONS },
|
|
44
|
+
plugins: {
|
|
45
|
+
admin: true,
|
|
46
|
+
anonymous: true,
|
|
47
|
+
apiKey: true,
|
|
48
|
+
convex: true,
|
|
49
|
+
// {{ORGANIZATIONS_SCHEMA}}
|
|
50
|
+
},
|
|
51
|
+
} satisfies BetterAuthSchemaInput
|
|
@@ -19,7 +19,7 @@ export const getSessionWithUser = query({
|
|
|
19
19
|
.withIndex("by_token", (q) => q.eq("token", args.sessionToken))
|
|
20
20
|
.first()
|
|
21
21
|
|
|
22
|
-
if (!session) return null
|
|
22
|
+
if (!session) {return null}
|
|
23
23
|
|
|
24
24
|
if (session.expiresAt < Date.now()) {
|
|
25
25
|
console.error("[getSessionWithUser] Session Expired")
|
|
@@ -27,10 +27,10 @@ export const getSessionWithUser = query({
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const userId = session.userId
|
|
30
|
-
if (!userId) return null
|
|
30
|
+
if (!userId) {return null}
|
|
31
31
|
|
|
32
32
|
const user = await ctx.db.get(userId as Id<typeof TABLE_SLUG_USERS>)
|
|
33
|
-
if (!user) return null
|
|
33
|
+
if (!user) {return null}
|
|
34
34
|
|
|
35
35
|
return {
|
|
36
36
|
session: {
|
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
"allowSyntheticDefaultImports": true,
|
|
9
9
|
"target": "ESNext",
|
|
10
10
|
"lib": ["ES2023", "dom", "dom.iterable"],
|
|
11
|
+
"types": ["node"],
|
|
11
12
|
"forceConsistentCasingInFileNames": true,
|
|
12
13
|
"module": "ESNext",
|
|
13
14
|
"isolatedModules": true,
|
|
14
15
|
"noEmit": true,
|
|
15
|
-
"baseUrl": ".",
|
|
16
16
|
"paths": {
|
|
17
17
|
"~/*": ["../src/*"],
|
|
18
18
|
"@convex/*": ["./*"]
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
-
"include": ["./**/*"],
|
|
21
|
+
"include": ["./**/*", "../src/vex.types.ts"],
|
|
22
22
|
"exclude": ["./_generated"]
|
|
23
23
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ConvexError } from "convex/values"
|
|
2
2
|
|
|
3
|
-
import { mutation, query } from "../_generated/server"
|
|
4
|
-
|
|
5
3
|
import { TABLE_SLUG_USERS } from "~/db/constants"
|
|
6
4
|
import { USER_ROLES } from "~/db/constants/auth"
|
|
7
5
|
|
|
6
|
+
import { mutation, query } from "../_generated/server"
|
|
7
|
+
|
|
8
8
|
/**
|
|
9
9
|
* Check whether the admin panel has been bootstrapped (at least one admin
|
|
10
10
|
* exists). Used by `WelcomePage` to decide between "Sign Up" and "Sign In".
|
|
@@ -2,9 +2,10 @@ import { createGetAuth } from "@vexcms/better-auth";
|
|
|
2
2
|
import { globalsApi } from "@vexcms/core/server";
|
|
3
3
|
|
|
4
4
|
import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
|
|
5
|
-
import config from "~/vex.config";
|
|
5
|
+
import config from "~/vex.config.server";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { query } from "../_generated/server";
|
|
8
|
+
import { vexMutation as mutation } from "../vex";
|
|
8
9
|
|
|
9
10
|
export const { get, find, upsert } = globalsApi({
|
|
10
11
|
config,
|
|
@@ -2,9 +2,10 @@ import { createGetAuth } from "@vexcms/better-auth";
|
|
|
2
2
|
import { mediaApi } from "@vexcms/core";
|
|
3
3
|
|
|
4
4
|
import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
|
|
5
|
-
import config from "~/vex.config";
|
|
5
|
+
import config from "~/vex.config.server";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { query } from "../_generated/server";
|
|
8
|
+
import { vexMutation as mutation } from "../vex";
|
|
8
9
|
|
|
9
10
|
export const { getUrl, generateUploadUrl, createMediaDocument, deleteMedia } = mediaApi({
|
|
10
11
|
config,
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { createGetAuth } from "@vexcms/better-auth";
|
|
2
|
-
import { collectionsApi } from "@vexcms/core/server";
|
|
2
|
+
import { collectionsApi, createVexMutations } from "@vexcms/core/server";
|
|
3
3
|
|
|
4
4
|
import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
|
|
5
|
-
import config from "~/vex.config";
|
|
5
|
+
import config from "~/vex.config.server";
|
|
6
6
|
|
|
7
|
-
import { mutation, query } from "./_generated/server";
|
|
7
|
+
import { internalMutation, mutation, query } from "./_generated/server";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Trigger-wrapped `mutation`/`internalMutation` builders — every write made
|
|
11
|
+
* through them fires the written collection's `afterChange`/`afterDelete`
|
|
12
|
+
* hooks (`@vexcms/core`'s `createVexMutations`). Used, not exported: this
|
|
13
|
+
* file's own `create`/`update`/`remove` (below) and `globals.ts`/`media.ts`
|
|
14
|
+
* build their mutations on `vexMutation`, never the raw builder above.
|
|
15
|
+
*/
|
|
16
|
+
export const { mutation: vexMutation, internalMutation: vexInternalMutation } = createVexMutations({
|
|
17
|
+
config,
|
|
18
|
+
mutation,
|
|
19
|
+
internalMutation,
|
|
20
|
+
});
|
|
8
21
|
|
|
9
22
|
export const { find, get, search, create, update, remove } = collectionsApi({
|
|
10
23
|
config,
|
|
11
24
|
query,
|
|
12
|
-
mutation,
|
|
25
|
+
mutation: vexMutation,
|
|
13
26
|
getAuth: createGetAuth({
|
|
14
27
|
orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS,
|
|
15
28
|
userCollectionSlug: TABLE_SLUG_USERS,
|
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
import type { NextConfig } from "next"
|
|
2
2
|
|
|
3
|
-
import "
|
|
3
|
+
import { existsSync } from "node:fs"
|
|
4
|
+
import { dirname, join, resolve } from "node:path"
|
|
5
|
+
|
|
6
|
+
import { env } from "./src/env.mjs"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Walks up from the app directory looking for `pnpm-workspace.yaml`.
|
|
10
|
+
*
|
|
11
|
+
* @param from - Directory to start from.
|
|
12
|
+
* @returns The workspace root when this app is a workspace member, otherwise
|
|
13
|
+
* `from` unchanged.
|
|
14
|
+
*/
|
|
15
|
+
function findWorkspaceRoot(from: string): string {
|
|
16
|
+
let dir = resolve(from)
|
|
17
|
+
for (;;) {
|
|
18
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) {return dir}
|
|
19
|
+
const parent = dirname(dir)
|
|
20
|
+
if (parent === dir) {return resolve(from)}
|
|
21
|
+
dir = parent
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Convex file storage URLs (`next/image` sources for uploaded media) are
|
|
26
|
+
// served from `<deployment>.convex.cloud` — derive the hostname from the
|
|
27
|
+
// validated NEXT_PUBLIC_CONVEX_URL so scaffolds work without a manual
|
|
28
|
+
// next.config.ts edit. Falls back to an empty list when the URL is unset or
|
|
29
|
+
// unparsable (e.g. a deployment-less build with SKIP_ENV_VALIDATION set).
|
|
30
|
+
let convexImageRemotePatterns: NonNullable<NextConfig["images"]>["remotePatterns"] = []
|
|
31
|
+
try {
|
|
32
|
+
convexImageRemotePatterns = [{ hostname: new URL(env.NEXT_PUBLIC_CONVEX_URL).hostname }]
|
|
33
|
+
} catch {
|
|
34
|
+
// No deployment yet — leave remotePatterns empty.
|
|
35
|
+
}
|
|
4
36
|
|
|
5
37
|
const nextConfig: NextConfig = {
|
|
6
38
|
devIndicators: {
|
|
@@ -8,12 +40,26 @@ const nextConfig: NextConfig = {
|
|
|
8
40
|
},
|
|
9
41
|
allowedDevOrigins: ["127.0.01", "localhost"],
|
|
10
42
|
reactCompiler: true,
|
|
43
|
+
// Turbopack's root sets the file-tracing root AND Tailwind's source-scan
|
|
44
|
+
// root, so it has to be the directory that actually owns `node_modules`.
|
|
45
|
+
//
|
|
46
|
+
// Standalone scaffold: that is this app. Pinning it stops Next walking up
|
|
47
|
+
// and adopting an unrelated outer lockfile.
|
|
48
|
+
//
|
|
49
|
+
// pnpm workspace member: it is the workspace root — dependencies are
|
|
50
|
+
// hoisted into the root virtual store and `next` is only reachable from
|
|
51
|
+
// there. Pinning to the app dir makes the build fail with "Could not find
|
|
52
|
+
// the Next.js package".
|
|
53
|
+
//
|
|
54
|
+
// Detecting `pnpm-workspace.yaml` picks the right answer in both cases
|
|
55
|
+
// without the template needing a monorepo-specific variant.
|
|
56
|
+
turbopack: {
|
|
57
|
+
root: findWorkspaceRoot(import.meta.dirname),
|
|
58
|
+
},
|
|
11
59
|
images: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// e.g. { hostname: "your-deployment-575.convex.cloud" }.
|
|
16
|
-
],
|
|
60
|
+
// Additional remote image hosts (e.g. an external CMS or CDN) can be
|
|
61
|
+
// added here alongside the auto-derived Convex hostname above.
|
|
62
|
+
remotePatterns: convexImageRemotePatterns,
|
|
17
63
|
},
|
|
18
64
|
}
|
|
19
65
|
|
|
@@ -24,18 +24,17 @@
|
|
|
24
24
|
"@t3-oss/env-nextjs": "0.13.11",
|
|
25
25
|
"@tanstack/react-form": "1.33.1",
|
|
26
26
|
"@tanstack/react-query": "5.101.2",
|
|
27
|
-
"@vexcms/better-auth": "~0.1.0-alpha.
|
|
28
|
-
"@vexcms/core": "~0.1.0-alpha.
|
|
29
|
-
"@vexcms/file-storage-convex": "~0.1.0-alpha.
|
|
30
|
-
"@vexcms/next": "~0.1.0-alpha.
|
|
31
|
-
"@vexcms/react": "~0.1.0-alpha.
|
|
27
|
+
"@vexcms/better-auth": "~0.1.0-alpha.21",
|
|
28
|
+
"@vexcms/core": "~0.1.0-alpha.21",
|
|
29
|
+
"@vexcms/file-storage-convex": "~0.1.0-alpha.21",
|
|
30
|
+
"@vexcms/next": "~0.1.0-alpha.21",
|
|
31
|
+
"@vexcms/react": "~0.1.0-alpha.21",
|
|
32
32
|
"better-auth": "1.6.23",
|
|
33
33
|
"class-variance-authority": "0.7.1",
|
|
34
34
|
"clsx": "2.1.1",
|
|
35
35
|
"convex": "1.44.0",
|
|
36
36
|
"convex-helpers": "0.1.120",
|
|
37
37
|
"lucide-react": "0.577.0",
|
|
38
|
-
"motion": "13.1.1",
|
|
39
38
|
"next": "16.3.3",
|
|
40
39
|
"nuqs": "2.9.0",
|
|
41
40
|
"react": "19.2.7",
|
|
@@ -53,7 +52,7 @@
|
|
|
53
52
|
"@types/node": "20.19.43",
|
|
54
53
|
"@types/react": "19.2.17",
|
|
55
54
|
"@types/react-dom": "19.2.3",
|
|
56
|
-
"@vexcms/cli": "~0.1.0-alpha.
|
|
55
|
+
"@vexcms/cli": "~0.1.0-alpha.21",
|
|
57
56
|
"babel-plugin-react-compiler": "1.0.0",
|
|
58
57
|
"eslint": "9.39.5",
|
|
59
58
|
"eslint-config-next": "15.5.9",
|
|
@@ -64,5 +63,17 @@
|
|
|
64
63
|
"tailwindcss": "4.3.2",
|
|
65
64
|
"typescript": "6.0.3",
|
|
66
65
|
"typescript-eslint": "8.63.0"
|
|
66
|
+
},
|
|
67
|
+
"pnpm": {
|
|
68
|
+
"overrides": {
|
|
69
|
+
"@better-auth/passkey": "1.6.23"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencyRules": {
|
|
72
|
+
"allowedVersions": {
|
|
73
|
+
"@convex-dev/better-auth>better-auth": "1.6.23",
|
|
74
|
+
"better-call": "2",
|
|
75
|
+
"typescript": "6"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
67
78
|
}
|
|
68
79
|
}
|
|
Binary file
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { AuthView } from "@daveyplate/better-auth-ui";
|
|
4
|
-
import { useSearchParams } from "next/navigation";
|
|
4
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
5
|
import { Activity } from "react";
|
|
6
6
|
|
|
7
7
|
import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog";
|
|
8
8
|
|
|
9
9
|
export default function AuthCard({ pathname }: { pathname: string }) {
|
|
10
|
+
const router = useRouter();
|
|
10
11
|
const searchParams = useSearchParams();
|
|
11
12
|
const isGuardRedirect = searchParams.has("redirectTo");
|
|
12
13
|
if (isGuardRedirect) {
|
|
@@ -16,20 +17,31 @@ export default function AuthCard({ pathname }: { pathname: string }) {
|
|
|
16
17
|
</main>
|
|
17
18
|
);
|
|
18
19
|
}
|
|
20
|
+
// No in-flow wrapper here on purpose. The Dialog portals into <body>, so a
|
|
21
|
+
// wrapping <main> would be an empty element sitting after the footer (the
|
|
22
|
+
// @auth slot renders last in the frontend layout). Next's post-navigation
|
|
23
|
+
// scroll handler targets the new segment's first DOM node and would scroll
|
|
24
|
+
// that empty element into view — i.e. jump the page to the bottom behind
|
|
25
|
+
// the modal. With only a portal rendered there is no node to target.
|
|
19
26
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
<Dialog
|
|
28
|
+
onOpenChange={(open) => {
|
|
29
|
+
if (!open) {
|
|
30
|
+
router.back();
|
|
31
|
+
}
|
|
32
|
+
}}
|
|
33
|
+
open
|
|
34
|
+
>
|
|
35
|
+
<Activity mode="hidden">
|
|
36
|
+
<DialogTitle />
|
|
37
|
+
</Activity>
|
|
38
|
+
<DialogContent
|
|
39
|
+
aria-describedby={undefined}
|
|
40
|
+
className="grid place-items-center bg-transparent shadow-none ring-transparent"
|
|
41
|
+
showCloseButton={false}
|
|
42
|
+
>
|
|
43
|
+
<AuthView path={pathname} />
|
|
44
|
+
</DialogContent>
|
|
45
|
+
</Dialog>
|
|
34
46
|
);
|
|
35
47
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { api } from "@convex/_generated/api";
|
|
2
|
-
import { NextAdminPage } from "@vexcms/next/server";
|
|
3
2
|
import { canAccessAdminPanel } from "@vexcms/core";
|
|
3
|
+
import { NextAdminPage } from "@vexcms/next/server";
|
|
4
4
|
import { redirect } from "next/navigation";
|
|
5
5
|
|
|
6
6
|
import { fetchAuthQuery, getToken } from "~/auth/server";
|
|
7
|
-
import config from "~/vex.config";
|
|
7
|
+
import config from "~/vex.config.server";
|
|
8
8
|
|
|
9
9
|
export const dynamic = "force-dynamic";
|
|
10
10
|
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { StorageAdapterContextProvider, VexAccessProvider } from "@vexcms/react";
|
|
3
|
+
import { VexConfigProvider } from "@vexcms/react"
|
|
5
4
|
|
|
6
|
-
import
|
|
5
|
+
import config from "~/vex.config"
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
8
|
+
* Mounts the single `VexConfigProvider` for the admin panel's client tree.
|
|
10
9
|
*
|
|
11
|
-
* This is a "use client" component so
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* This is a "use client" component so `vex.config` is imported and owned on
|
|
11
|
+
* the client — it never crosses the RSC boundary as a prop. `access` and
|
|
12
|
+
* `storage.clientUploads` (the two fields that used to need their own
|
|
13
|
+
* providers) are already part of the config this imports.
|
|
15
14
|
*/
|
|
16
15
|
export function ClientProviders({ children }: { children: React.ReactNode }) {
|
|
17
|
-
return
|
|
18
|
-
<StorageAdapterContextProvider adapterClients={{ convex: uploadFile }}>
|
|
19
|
-
<VexAccessProvider access={access}>{children}</VexAccessProvider>
|
|
20
|
-
</StorageAdapterContextProvider>
|
|
21
|
-
);
|
|
16
|
+
return <VexConfigProvider config={config}>{children}</VexConfigProvider>
|
|
22
17
|
}
|
|
@@ -2,18 +2,20 @@ import type { ReactNode } from "react"
|
|
|
2
2
|
|
|
3
3
|
import { NextAdminLayout } from "@vexcms/next/client"
|
|
4
4
|
|
|
5
|
+
import { AuthServerProvider } from "~/components/providers/auth"
|
|
5
6
|
import { getCurrentUser } from "~/auth/serverUtils"
|
|
6
|
-
import config from "~/vex.config"
|
|
7
7
|
|
|
8
8
|
import { ClientProviders } from "./clientProviders"
|
|
9
9
|
|
|
10
10
|
export default async function AdminLayout({ children }: { children: ReactNode }) {
|
|
11
11
|
const user = await getCurrentUser()
|
|
12
12
|
return (
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
<AuthServerProvider>
|
|
14
|
+
<ClientProviders>
|
|
15
|
+
<NextAdminLayout user={user ?? undefined}>
|
|
16
|
+
{children}
|
|
17
|
+
</NextAdminLayout>
|
|
18
|
+
</ClientProviders>
|
|
19
|
+
</AuthServerProvider>
|
|
18
20
|
)
|
|
19
21
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { api } from "@convex/_generated/api"
|
|
2
|
+
import { createVexRevalidateRoute } from "@vexcms/next/cache"
|
|
3
|
+
|
|
4
|
+
import { fetchAuthQuery, getToken } from "~/auth/server"
|
|
5
|
+
import config from "~/vex.config.server"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `POST /api/vex/revalidate` — session-authorized path purge.
|
|
9
|
+
*
|
|
10
|
+
* Called by `useVexMutation` after every admin-panel write, same origin, and
|
|
11
|
+
* by the admin panel's Revalidate control over the same session. Never
|
|
12
|
+
* secret-authorized: the caller must hold a real session with write
|
|
13
|
+
* permission on the affected collection.
|
|
14
|
+
*/
|
|
15
|
+
export const { POST } = createVexRevalidateRoute({
|
|
16
|
+
config,
|
|
17
|
+
getToken,
|
|
18
|
+
getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),
|
|
19
|
+
})
|
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
@import "shadcn/tailwind.css";
|
|
26
26
|
@import "@vexcms/next/styles";
|
|
27
27
|
|
|
28
|
-
/*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@source "
|
|
36
|
-
@source "
|
|
28
|
+
/* Scan the installed @vexcms packages for class candidates (their dist output
|
|
29
|
+
ships alongside this app in node_modules; @vexcms/next/styles above pulls
|
|
30
|
+
the stylesheet, these make sure every utility used by the admin UI exists).
|
|
31
|
+
Globbed to `*.js` only — the same `dist` directories also carry `.map`
|
|
32
|
+
sourcemaps and `.d.ts` declaration files, and Tailwind's scanner extracts
|
|
33
|
+
garbage "class candidates" from their base64/binary-ish content and emits
|
|
34
|
+
unparseable CSS, exactly like the raw `public/` binary assets below. */
|
|
35
|
+
@source "../../node_modules/@vexcms/react/dist/**/*.js";
|
|
36
|
+
@source "../../node_modules/@vexcms/next/dist/**/*.js";
|
|
37
|
+
@source not "../../public";
|
|
37
38
|
|
|
38
39
|
@custom-variant dark (&:is(.dark *));
|
|
39
40
|
|
|
@@ -2,6 +2,7 @@ import type { Metadata } from "next"
|
|
|
2
2
|
|
|
3
3
|
import "./globals.css"
|
|
4
4
|
|
|
5
|
+
import { ThemeScript } from "@vexcms/react"
|
|
5
6
|
import { Geist, Geist_Mono } from "next/font/google"
|
|
6
7
|
|
|
7
8
|
import ClientProviders from "~/components/providers/client"
|
|
@@ -30,6 +31,11 @@ export default function RootLayout({
|
|
|
30
31
|
}>) {
|
|
31
32
|
return (
|
|
32
33
|
<html className={`${geistSans.variable} ${geistMono.variable} antialiased`} lang="en" suppressHydrationWarning>
|
|
34
|
+
<head>
|
|
35
|
+
{/* Applies the persisted light/dark class before first paint —
|
|
36
|
+
without it the admin flashes light before the saved mode lands. */}
|
|
37
|
+
<ThemeScript />
|
|
38
|
+
</head>
|
|
33
39
|
<body>
|
|
34
40
|
<ServerProviders>
|
|
35
41
|
<ClientProviders>{children}</ClientProviders>
|