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
|
@@ -11,8 +11,16 @@ import { images, users } from "~/vexcms/collections"
|
|
|
11
11
|
* Access control (RBAC) for the admin panel and every registered collection.
|
|
12
12
|
*
|
|
13
13
|
* `admin` gets unrestricted access. `user` can read and update only their own
|
|
14
|
-
* profile row
|
|
15
|
-
* you register a new collection in `vex.config.ts`.
|
|
14
|
+
* profile row, cannot reach the admin panel, and can read `images`. Add a
|
|
15
|
+
* resource here whenever you register a new collection in `vex.config.ts`.
|
|
16
|
+
*
|
|
17
|
+
* `anonRole: user` also covers every unauthenticated public-site request,
|
|
18
|
+
* including the build-time metadata fetch. `images.read` is what lets the
|
|
19
|
+
* public site show uploaded media at all: `MediaImage` and
|
|
20
|
+
* `src/lib/metadata.ts` resolve uploads through the RBAC-gated
|
|
21
|
+
* `vex/media:getUrl`, so without it anonymous visitors get no images and the
|
|
22
|
+
* page ships no `og:image`. Narrow it with a `constraints` rule if some media
|
|
23
|
+
* must stay private; do not remove it.
|
|
16
24
|
*
|
|
17
25
|
* @see https://vexcms.dev/docs/access-control
|
|
18
26
|
*/
|
|
@@ -32,6 +40,10 @@ export const access = defineAccess({
|
|
|
32
40
|
adminPanel: {
|
|
33
41
|
access: false,
|
|
34
42
|
},
|
|
43
|
+
images: {
|
|
44
|
+
"*": false,
|
|
45
|
+
read: true,
|
|
46
|
+
},
|
|
35
47
|
user: {
|
|
36
48
|
"*": false,
|
|
37
49
|
read: {
|
|
@@ -2,11 +2,25 @@ import { api } from "@convex/_generated/api"
|
|
|
2
2
|
import { fetchQuery } from "convex/nextjs"
|
|
3
3
|
import { cookies } from "next/headers"
|
|
4
4
|
|
|
5
|
+
const SESSION_COOKIE = "better-auth.session_token"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read the Better Auth session token from cookies.
|
|
9
|
+
*
|
|
10
|
+
* Better Auth prefixes its cookies with `__Secure-` whenever the resolved
|
|
11
|
+
* `baseURL` is https. Locally over http the cookie is
|
|
12
|
+
* `better-auth.session_token`; on a real domain it is
|
|
13
|
+
* `__Secure-better-auth.session_token`. Reading only the bare name works in
|
|
14
|
+
* development and silently returns `null` in production — no error, just an
|
|
15
|
+
* anonymous caller. Check the prefixed name first, as Better Auth's own reader
|
|
16
|
+
* does.
|
|
17
|
+
*/
|
|
5
18
|
export async function getSessionToken() {
|
|
6
19
|
const cookieStore = await cookies()
|
|
7
|
-
const sessionTokenCookie =
|
|
20
|
+
const sessionTokenCookie =
|
|
21
|
+
cookieStore.get(`__Secure-${SESSION_COOKIE}`)?.value ?? cookieStore.get(SESSION_COOKIE)?.value
|
|
8
22
|
|
|
9
|
-
if (!sessionTokenCookie) return null
|
|
23
|
+
if (!sessionTokenCookie) {return null}
|
|
10
24
|
|
|
11
25
|
// better-auth stores tokens as "<raw_token>.<hmac_signature>" (2 parts, not a JWT)
|
|
12
26
|
// The raw token (first part) is what's indexed in the DB by_token
|
|
@@ -24,12 +38,12 @@ export async function getSessionToken() {
|
|
|
24
38
|
export const getCurrentUser = async () => {
|
|
25
39
|
try {
|
|
26
40
|
const sessionToken = await getSessionToken()
|
|
27
|
-
if (!sessionToken) return null
|
|
41
|
+
if (!sessionToken) {return null}
|
|
28
42
|
|
|
29
43
|
const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
|
|
30
44
|
sessionToken,
|
|
31
45
|
})
|
|
32
|
-
if (!session?.user) return null
|
|
46
|
+
if (!session?.user) {return null}
|
|
33
47
|
return session.user
|
|
34
48
|
} catch (error) {
|
|
35
49
|
console.error("Error getting current user:", error)
|
|
@@ -43,12 +57,12 @@ export const getCurrentUser = async () => {
|
|
|
43
57
|
export async function getSession() {
|
|
44
58
|
try {
|
|
45
59
|
const sessionToken = await getSessionToken()
|
|
46
|
-
if (!sessionToken) return null
|
|
60
|
+
if (!sessionToken) {return null}
|
|
47
61
|
|
|
48
62
|
const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
|
|
49
63
|
sessionToken,
|
|
50
64
|
})
|
|
51
|
-
if (!session?.user) return null
|
|
65
|
+
if (!session?.user) {return null}
|
|
52
66
|
return session
|
|
53
67
|
} catch (error) {
|
|
54
68
|
console.error("[getSession] Error:", error)
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { api } from "@convex/_generated/api"
|
|
3
4
|
import { useMutation, useQuery } from "convex/react"
|
|
4
5
|
import { usePathname, useRouter } from "next/navigation"
|
|
5
|
-
import { useEffect, useState } from "react"
|
|
6
|
+
import { useEffect, useRef, useState } from "react"
|
|
6
7
|
|
|
7
|
-
import { api } from "@convex/_generated/api"
|
|
8
8
|
import { useSession } from "~/auth/client"
|
|
9
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
|
|
15
|
-
className="opacity-75"
|
|
16
|
-
fill="currentColor"
|
|
17
|
-
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"
|
|
18
|
-
/>
|
|
19
|
-
</svg>
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
10
|
/**
|
|
24
11
|
* Landing UI for a project that has not been bootstrapped yet (no admin user),
|
|
25
12
|
* and the "Go to Admin" / "Sign in" entry point once it has.
|
|
@@ -41,33 +28,44 @@ export function WelcomePage() {
|
|
|
41
28
|
const [promoting, setPromoting] = useState(false)
|
|
42
29
|
const [navigating, setNavigating] = useState(false)
|
|
43
30
|
|
|
44
|
-
// Reset navigating
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
// Reset `navigating` when the user lands back on the home page (e.g. they
|
|
32
|
+
// dismissed the intercepting auth dialog). Adjusting state during render on
|
|
33
|
+
// a changed input is the pattern React documents for this; an effect would
|
|
34
|
+
// paint the stale spinner for a frame first.
|
|
35
|
+
const [lastPathname, setLastPathname] = useState(pathname)
|
|
36
|
+
if (lastPathname !== pathname) {
|
|
37
|
+
setLastPathname(pathname)
|
|
38
|
+
if (pathname === "/") {setNavigating(false)}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Run-once latch. `promoting` and `navigating` both render, so they stay
|
|
42
|
+
// state; the guard does not, so it is a ref — that keeps it out of the
|
|
43
|
+
// dependency array and stops the effect re-running just to re-read it.
|
|
44
|
+
const promotingRef = useRef(false)
|
|
51
45
|
|
|
52
|
-
// If the user is signed in and this is a fresh project, try to promote them
|
|
53
46
|
useEffect(() => {
|
|
54
|
-
if (session?.user
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
47
|
+
if (!session?.user || isBootstrapped !== false || promotingRef.current) {return}
|
|
48
|
+
|
|
49
|
+
promotingRef.current = true
|
|
50
|
+
setPromoting(true)
|
|
51
|
+
setNavigating(true)
|
|
52
|
+
|
|
53
|
+
promoteFirstAdmin()
|
|
54
|
+
.then((result) => {
|
|
55
|
+
if (result.promoted) {
|
|
56
|
+
router.push("/admin")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
promotingRef.current = false
|
|
60
|
+
setPromoting(false)
|
|
61
|
+
setNavigating(false)
|
|
62
|
+
})
|
|
63
|
+
.catch(() => {
|
|
64
|
+
promotingRef.current = false
|
|
65
|
+
setPromoting(false)
|
|
66
|
+
setNavigating(false)
|
|
67
|
+
})
|
|
68
|
+
}, [session, isBootstrapped, promoteFirstAdmin, router])
|
|
71
69
|
|
|
72
70
|
// Treat undefined (query still loading) as not bootstrapped — show the welcome page immediately
|
|
73
71
|
const bootstrapped = isBootstrapped === true
|
|
@@ -93,17 +91,17 @@ export function WelcomePage() {
|
|
|
93
91
|
|
|
94
92
|
<div className="flex gap-4">
|
|
95
93
|
{session?.user ? (
|
|
96
|
-
<button onClick={() => handleNavigate("/admin")}
|
|
94
|
+
<button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/admin")}>
|
|
97
95
|
{navigating && <LoadingSpinner />}
|
|
98
96
|
{navigating ? "Loading..." : "Go to Admin Panel"}
|
|
99
97
|
</button>
|
|
100
98
|
) : bootstrapped ? (
|
|
101
|
-
<button onClick={() => handleNavigate("/auth/sign-in")}
|
|
99
|
+
<button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/auth/sign-in")}>
|
|
102
100
|
{navigating && <LoadingSpinner />}
|
|
103
101
|
{navigating ? "Loading..." : "Sign In"}
|
|
104
102
|
</button>
|
|
105
103
|
) : (
|
|
106
|
-
<button onClick={() => handleNavigate("/auth/sign-up")}
|
|
104
|
+
<button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/auth/sign-up")}>
|
|
107
105
|
{navigating && <LoadingSpinner />}
|
|
108
106
|
{navigating ? "Loading..." : "Create Admin Account"}
|
|
109
107
|
</button>
|
|
@@ -114,3 +112,16 @@ export function WelcomePage() {
|
|
|
114
112
|
</div>
|
|
115
113
|
)
|
|
116
114
|
}
|
|
115
|
+
|
|
116
|
+
function LoadingSpinner() {
|
|
117
|
+
return (
|
|
118
|
+
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
119
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
120
|
+
<path
|
|
121
|
+
className="opacity-75"
|
|
122
|
+
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"
|
|
123
|
+
fill="currentColor"
|
|
124
|
+
/>
|
|
125
|
+
</svg>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -2,17 +2,25 @@ import { ThemeProvider } from "@vexcms/react";
|
|
|
2
2
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
|
3
3
|
import { type PropsWithChildren } from "react";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Server-side provider shell: theme context + the nuqs URL-state adapter.
|
|
7
|
+
*
|
|
8
|
+
* `AuthServerProvider` used to wrap `NuqsAdapter` here, putting a `getToken()`
|
|
9
|
+
* cookie read on every route's render path. It now mounts directly in
|
|
10
|
+
* `app/(vexcms)/admin/layout.tsx`, the only route group that needs it.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately does **not** mount `ConvexClientProvider` — `ClientProviders`
|
|
13
|
+
* renders it, and `ClientProviders` is nested inside this component, so its
|
|
14
|
+
* copy is the one that actually reaches `children`. Mounting it here as well
|
|
15
|
+
* built a second `ConvexReactClient` + `QueryClient` pair on every server
|
|
16
|
+
* render (`providers/convex.tsx` intentionally creates fresh clients per call
|
|
17
|
+
* server-side to avoid cross-request leaks) whose only consumer was the
|
|
18
|
+
* discarded outer subtree.
|
|
19
|
+
*/
|
|
8
20
|
export default function ServerProviders({ children }: PropsWithChildren) {
|
|
9
21
|
return (
|
|
10
22
|
<ThemeProvider defaultTheme="system">
|
|
11
|
-
<
|
|
12
|
-
<AuthServerProvider>
|
|
13
|
-
<NuqsAdapter>{children}</NuqsAdapter>
|
|
14
|
-
</AuthServerProvider>
|
|
15
|
-
</ConvexClientProvider>
|
|
23
|
+
<NuqsAdapter>{children}</NuqsAdapter>
|
|
16
24
|
</ThemeProvider>
|
|
17
25
|
);
|
|
18
26
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { betterAuthAdapter } from "@vexcms/better-auth"
|
|
2
|
+
import { defineServerConfig } from "@vexcms/core"
|
|
3
|
+
import { convexFileStorage } from "@vexcms/file-storage-convex"
|
|
4
|
+
|
|
5
|
+
import { authOptions } from "@convex/auth/options"
|
|
6
|
+
|
|
7
|
+
import vexConfig from "./vex.config"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* VexCMS server-only configuration for this project.
|
|
11
|
+
*
|
|
12
|
+
* Layers the Better Auth adapter and Convex file storage onto the
|
|
13
|
+
* client-safe `vexConfig`. You write this file once at scaffold time and
|
|
14
|
+
* rarely revisit it — day-to-day settings live in `vex.config.ts`.
|
|
15
|
+
*
|
|
16
|
+
* @see defineServerConfig in @vexcms/core
|
|
17
|
+
* @see betterAuthAdapter in @vexcms/better-auth
|
|
18
|
+
* @see ./vex.config for the client-safe half this layers onto
|
|
19
|
+
*/
|
|
20
|
+
const config = defineServerConfig({
|
|
21
|
+
config: vexConfig,
|
|
22
|
+
server: {
|
|
23
|
+
auth: { adapter: betterAuthAdapter({ config: authOptions }) },
|
|
24
|
+
storage: {
|
|
25
|
+
adapters: [convexFileStorage()],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export default config
|
|
@@ -1,35 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { betterAuthCollections } from "@vexcms/better-auth/client"
|
|
2
2
|
import { defineConfig } from "@vexcms/core"
|
|
3
|
-
import {
|
|
3
|
+
import { uploadFile } from "@vexcms/file-storage-convex/client"
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { authSchema } from "@convex/auth/schema"
|
|
6
6
|
|
|
7
7
|
import { access } from "~/auth/access"
|
|
8
8
|
import { images, users } from "~/vexcms/collections"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* VexCMS configuration for this project.
|
|
11
|
+
* VexCMS client-safe configuration for this project.
|
|
12
12
|
*
|
|
13
|
-
* Registers the admin sidebar layout
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Registers the admin sidebar layout and every collection, global, and
|
|
14
|
+
* media collection. Add collections/globals here as you define them —
|
|
15
|
+
* `vex dev` / `vex generate` derive the Convex schema and TypeScript types
|
|
16
|
+
* from this file, and the browser imports it directly via
|
|
17
|
+
* `VexConfigProvider`, so nothing here may reach a server SDK or an
|
|
18
|
+
* environment variable.
|
|
17
19
|
*
|
|
18
20
|
* @see defineConfig in @vexcms/core
|
|
19
|
-
* @see
|
|
21
|
+
* @see ./vex.config.server for the auth adapter and storage adapter wiring
|
|
20
22
|
*/
|
|
21
23
|
const vexConfig = defineConfig({
|
|
22
24
|
access,
|
|
25
|
+
authCollections: betterAuthCollections(authSchema),
|
|
23
26
|
admin: {
|
|
24
27
|
sidebar: {
|
|
25
28
|
side: "right",
|
|
26
29
|
},
|
|
27
30
|
},
|
|
28
|
-
authAdapter: betterAuthAdapter({ config: authOptions }),
|
|
29
31
|
storage: {
|
|
30
|
-
|
|
32
|
+
clientUploads: { convex: uploadFile },
|
|
31
33
|
},
|
|
32
34
|
collections: [users],
|
|
35
|
+
mediaCollections: [images],
|
|
33
36
|
})
|
|
34
37
|
|
|
35
38
|
export default vexConfig
|
|
@@ -135,6 +135,7 @@ export interface Image extends VexDocument {
|
|
|
135
135
|
src: string
|
|
136
136
|
width?: number
|
|
137
137
|
height?: number
|
|
138
|
+
updatedAt?: number
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
export type CollectionSlug =
|
|
@@ -263,7 +264,7 @@ declare module "@vexcms/core" {
|
|
|
263
264
|
}
|
|
264
265
|
images: {
|
|
265
266
|
text: "filename" | "alt" | "mimeType" | "storageId" | "src"
|
|
266
|
-
number: "size" | "width" | "height"
|
|
267
|
+
number: "size" | "width" | "height" | "updatedAt"
|
|
267
268
|
checkbox: "deleted"
|
|
268
269
|
}
|
|
269
270
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { defineCollection } from "@vexcms/core"
|
|
1
|
+
import { defineCollection, text } from "@vexcms/core"
|
|
2
2
|
|
|
3
3
|
import { TABLE_SLUG_USERS } from "~/db/constants"
|
|
4
4
|
|
|
5
5
|
export const users = defineCollection({
|
|
6
6
|
slug: TABLE_SLUG_USERS,
|
|
7
7
|
admin: {
|
|
8
|
+
useAsTitle: "name",
|
|
8
9
|
icon: "Users",
|
|
9
10
|
},
|
|
10
11
|
labels: {
|
|
@@ -12,6 +13,7 @@ export const users = defineCollection({
|
|
|
12
13
|
plural: "Users",
|
|
13
14
|
},
|
|
14
15
|
fields: {
|
|
16
|
+
name: text({ label: "Name", required: true }),
|
|
15
17
|
// Add custom user fields here — the auth adapter already contributes
|
|
16
18
|
// name/email/roles/etc. from Better Auth's schema.
|
|
17
19
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v } from "convex/values"
|
|
2
2
|
|
|
3
3
|
import { TABLE_SLUG_PAGES } from "~/db/constants"
|
|
4
|
-
import { find } from "~/vexcms/api"
|
|
4
|
+
import { find, publishedSlugs as readPublishedSlugs } from "~/vexcms/api"
|
|
5
5
|
|
|
6
6
|
import { query } from "./_generated/server"
|
|
7
7
|
|
|
@@ -29,3 +29,22 @@ export const getBySlug = query({
|
|
|
29
29
|
})
|
|
30
30
|
},
|
|
31
31
|
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns `{ slug, createdAt, updatedAt? }` for every page document.
|
|
35
|
+
*
|
|
36
|
+
* Consumed by `app/sitemap.ts` and `[slug]/page.tsx`'s `generateStaticParams`.
|
|
37
|
+
* Access is bypassed for the same reason `getBySlug` bypasses it: both are
|
|
38
|
+
* read at build time and by anonymous crawlers, neither of which carries a
|
|
39
|
+
* session.
|
|
40
|
+
*/
|
|
41
|
+
export const publishedSlugs = query({
|
|
42
|
+
args: {},
|
|
43
|
+
handler: async (ctx) => {
|
|
44
|
+
return await readPublishedSlugs({
|
|
45
|
+
access: { bypass: true },
|
|
46
|
+
collection: TABLE_SLUG_PAGES,
|
|
47
|
+
ctx,
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
})
|
|
@@ -5,14 +5,14 @@ import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_USERS } from "~/db/constants"
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
apikey,
|
|
8
|
+
footers,
|
|
9
|
+
headers,
|
|
8
10
|
images,
|
|
9
11
|
jwks,
|
|
10
|
-
session,
|
|
11
|
-
verification,
|
|
12
12
|
pages,
|
|
13
|
-
|
|
14
|
-
footers,
|
|
13
|
+
session,
|
|
15
14
|
themes,
|
|
15
|
+
verification,
|
|
16
16
|
} from "./vex.schema"
|
|
17
17
|
|
|
18
18
|
export default defineSchema({
|