create-vexcms 0.0.20 → 0.1.0-alpha.2
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/LICENSE +202 -0
- package/README.md +93 -83
- package/dist/index.js +246 -81
- package/dist/index.mjs +30976 -0
- package/package.json +35 -15
- package/templates/base-nextjs/README.md +53 -205
- package/templates/base-nextjs/_env.example +7 -9
- package/templates/base-nextjs/_gitignore +41 -1
- package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
- package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
- package/templates/base-nextjs/convex/_generated/server.js +8 -0
- package/templates/base-nextjs/convex/auth/api.ts +49 -3
- package/templates/base-nextjs/convex/auth/db.ts +42 -300
- package/templates/base-nextjs/convex/auth/index.ts +11 -7
- package/templates/base-nextjs/convex/auth/options.ts +5 -29
- package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
- package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
- package/templates/base-nextjs/convex/http.ts +18 -3
- package/templates/base-nextjs/convex/schema.ts +104 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
- package/templates/base-nextjs/convex/vex/globals.ts +19 -82
- package/templates/base-nextjs/convex/vex/media.ts +19 -246
- package/templates/base-nextjs/convex/vex.schema.ts +138 -0
- package/templates/base-nextjs/convex/vex.ts +19 -0
- package/templates/base-nextjs/convex.json +6 -0
- package/templates/base-nextjs/eslint.config.mjs +1 -21
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +47 -48
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
- package/templates/base-nextjs/src/app/globals.css +240 -104
- package/templates/base-nextjs/src/app/layout.tsx +23 -13
- package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
- package/templates/base-nextjs/src/auth/access.ts +46 -0
- package/templates/base-nextjs/src/auth/client.tsx +17 -5
- package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
- package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
- package/templates/base-nextjs/src/components/component-example.tsx +56 -464
- package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
- package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
- package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
- package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
- package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
- package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
- package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
- package/templates/base-nextjs/src/db/constants/index.ts +22 -35
- package/templates/base-nextjs/src/lib/utils.ts +0 -12
- package/templates/base-nextjs/src/proxy.ts +97 -12
- package/templates/base-nextjs/src/vex.config.ts +35 -0
- package/templates/base-nextjs/src/vex.types.ts +293 -0
- package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
- package/templates/base-nextjs/tsconfig.json +3 -0
- package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
- package/templates/marketing-site/convex/_generated/api.js +23 -0
- package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
- package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
- package/templates/marketing-site/convex/_generated/server.js +101 -0
- package/templates/marketing-site/convex/footers.ts +11 -7
- package/templates/marketing-site/convex/headers.ts +15 -5
- package/templates/marketing-site/convex/pages.ts +20 -35
- package/templates/marketing-site/convex/schema.ts +119 -0
- package/templates/marketing-site/convex/seed.ts +582 -35
- package/templates/marketing-site/convex/siteSettings.ts +19 -5
- package/templates/marketing-site/convex/theme.ts +55 -61
- package/templates/marketing-site/convex/vex.schema.ts +355 -0
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
- package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
- package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
- package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
- package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
- package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
- package/templates/marketing-site/src/lib/metadata.ts +51 -72
- package/templates/marketing-site/src/vex.config.ts +35 -0
- package/templates/marketing-site/src/vex.types.ts +634 -0
- package/templates/marketing-site/src/vexcms/api.ts +23 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
- package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
- package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
- package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
- package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
- package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
- package/templates/base-nextjs/convex/auth/config.ts +0 -10
- package/templates/base-nextjs/convex/vex/auth.ts +0 -38
- package/templates/base-nextjs/convex/vex/collections.ts +0 -333
- package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
- package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
- package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
- package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
- package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
- package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
- package/templates/base-nextjs/convex/vex/versions.ts +0 -648
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
- package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
- package/templates/base-nextjs/src/db/types.ts +0 -6
- package/templates/base-nextjs/src/vexcms/access.ts +0 -44
- package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
- package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
- package/templates/base-nextjs/vex.config.ts +0 -25
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
- package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
- package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
- package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
- package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
- package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
- package/templates/marketing-site/src/db/constants/index.ts +0 -52
- package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
- package/templates/marketing-site/src/vexcms/access.ts +0 -36
- package/templates/marketing-site/vex.config.ts +0 -32
|
@@ -1,186 +1,53 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { RenderBlocks } from "@vexcms/ui"
|
|
4
3
|
import { convexQuery } from "@convex-dev/react-query"
|
|
5
4
|
import { useQuery } from "@tanstack/react-query"
|
|
6
|
-
import { useMutation, useQuery as useConvexQuery } from "convex/react"
|
|
7
|
-
import Link from "next/link"
|
|
8
|
-
import { usePathname, useRouter } from "next/navigation"
|
|
9
|
-
import { useEffect, useState } from "react"
|
|
10
5
|
import { anyApi } from "convex/server"
|
|
11
6
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import {
|
|
7
|
+
import { RenderBlocks, type RenderableBlock } from "@vexcms/react"
|
|
8
|
+
|
|
9
|
+
import { WelcomePage } from "~/components/WelcomePage"
|
|
15
10
|
import { blockComponents } from "~/vexcms/blocks"
|
|
16
11
|
|
|
12
|
+
type PageBlockLike = RenderableBlock
|
|
13
|
+
|
|
14
|
+
export interface PageContentProps {
|
|
15
|
+
/** URL slug to render. Omit (or empty) for the home page. */
|
|
16
|
+
slug?: string
|
|
17
|
+
/** Server-fetched `pages.getBySlug` result, hydrated as the query's initial data. */
|
|
18
|
+
initialData?: Record<string, unknown>[]
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
22
|
+
* Renders one marketing page's blocks via `RenderBlocks` (Contract 1), or
|
|
23
|
+
* falls back to base's bootstrap `WelcomePage` when no `home` page document
|
|
24
|
+
* exists yet — a fresh scaffold before `pnpm seed` has run (Contract 3).
|
|
21
25
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
26
|
+
* `pages.getBySlug` always returns an array (empty when no match — the same
|
|
27
|
+
* shape every collection query returns), so this always reads `pages?.[0]`.
|
|
24
28
|
*/
|
|
25
|
-
export function PageContent({
|
|
26
|
-
slug
|
|
27
|
-
initialData,
|
|
28
|
-
}: {
|
|
29
|
-
slug?: string
|
|
30
|
-
initialData?: Record<string, unknown> | null
|
|
31
|
-
}) {
|
|
32
|
-
const normalizedSlug = normalizeSlug(slug)
|
|
29
|
+
export function PageContent({ slug, initialData }: PageContentProps) {
|
|
30
|
+
const normalizedSlug = slug && slug.length > 0 ? slug : "home"
|
|
33
31
|
|
|
34
|
-
const { data:
|
|
35
|
-
...convexQuery(anyApi.pages.getBySlug, {
|
|
36
|
-
|
|
37
|
-
_vexDrafts: false,
|
|
38
|
-
}),
|
|
39
|
-
initialData: initialData ?? undefined,
|
|
32
|
+
const { data: pages, isPending } = useQuery({
|
|
33
|
+
...convexQuery(anyApi.pages.getBySlug, { slug: normalizedSlug }),
|
|
34
|
+
initialData,
|
|
40
35
|
})
|
|
41
36
|
|
|
37
|
+
const page = pages?.[0]
|
|
38
|
+
|
|
42
39
|
if (isPending && initialData === undefined) {
|
|
43
|
-
return
|
|
44
|
-
<div className="flex min-h-screen items-center justify-center">
|
|
45
|
-
<p className="text-muted-foreground">Loading...</p>
|
|
46
|
-
</div>
|
|
47
|
-
)
|
|
40
|
+
return null
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
if (!page) {
|
|
51
|
-
|
|
52
|
-
return <WelcomePage />
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<div className="mx-auto max-w-3xl px-4 py-12">
|
|
57
|
-
<h1 className="text-2xl font-bold">Page not found</h1>
|
|
58
|
-
<p className="mt-2 text-muted-foreground">
|
|
59
|
-
The page “{slug}” doesn't exist or hasn't been
|
|
60
|
-
published yet.
|
|
61
|
-
</p>
|
|
62
|
-
<Link
|
|
63
|
-
className="mt-4 inline-block text-sm text-primary hover:underline"
|
|
64
|
-
href="/"
|
|
65
|
-
>
|
|
66
|
-
← Back to home
|
|
67
|
-
</Link>
|
|
68
|
-
</div>
|
|
69
|
-
)
|
|
44
|
+
return <WelcomePage />
|
|
70
45
|
}
|
|
71
46
|
|
|
72
47
|
return (
|
|
73
48
|
<RenderBlocks
|
|
74
|
-
blocks={page.
|
|
49
|
+
blocks={page.blocks as PageBlockLike[] | null | undefined}
|
|
75
50
|
components={blockComponents}
|
|
76
51
|
/>
|
|
77
52
|
)
|
|
78
53
|
}
|
|
79
|
-
|
|
80
|
-
function LoadingSpinner() {
|
|
81
|
-
return (
|
|
82
|
-
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
83
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
84
|
-
<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" />
|
|
85
|
-
</svg>
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Welcome page shown on fresh projects with no "home" page.
|
|
91
|
-
* Handles the first-user bootstrap flow: sign up → promote to admin → redirect to admin panel.
|
|
92
|
-
*/
|
|
93
|
-
function WelcomePage() {
|
|
94
|
-
const isBootstrapped = useConvexQuery(api.vex.firstUser.isBootstrapped)
|
|
95
|
-
const promoteFirstAdmin = useMutation(api.vex.firstUser.promoteFirstAdmin)
|
|
96
|
-
const { data: session } = useSession()
|
|
97
|
-
const router = useRouter()
|
|
98
|
-
const pathname = usePathname()
|
|
99
|
-
const [promoting, setPromoting] = useState(false)
|
|
100
|
-
const [navigating, setNavigating] = useState(false)
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
if (pathname === "/") {
|
|
104
|
-
setNavigating(false)
|
|
105
|
-
}
|
|
106
|
-
}, [pathname])
|
|
107
|
-
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
if (session?.user && isBootstrapped === false && !promoting) {
|
|
110
|
-
setPromoting(true)
|
|
111
|
-
setNavigating(true)
|
|
112
|
-
promoteFirstAdmin()
|
|
113
|
-
.then((result) => {
|
|
114
|
-
if (result.promoted) {
|
|
115
|
-
router.push("/admin")
|
|
116
|
-
} else {
|
|
117
|
-
setNavigating(false)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
.catch(() => {
|
|
121
|
-
setPromoting(false)
|
|
122
|
-
setNavigating(false)
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
}, [session, isBootstrapped, promoting, promoteFirstAdmin, router])
|
|
126
|
-
|
|
127
|
-
const bootstrapped = isBootstrapped === true
|
|
128
|
-
|
|
129
|
-
const buttonClass =
|
|
130
|
-
"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"
|
|
131
|
-
|
|
132
|
-
const handleNavigate = (path: string) => {
|
|
133
|
-
setNavigating(true)
|
|
134
|
-
router.push(path)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<div className="flex min-h-screen flex-col items-center justify-center gap-8 p-8">
|
|
139
|
-
<div className="text-center">
|
|
140
|
-
<h1 className="text-4xl font-bold tracking-tight">VEX CMS</h1>
|
|
141
|
-
<p className="mt-3 text-lg text-muted-foreground">
|
|
142
|
-
{bootstrapped
|
|
143
|
-
? "Your content management system is ready."
|
|
144
|
-
: "Welcome! Create your admin account to get started."}
|
|
145
|
-
</p>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<div className="flex gap-4">
|
|
149
|
-
{session?.user ? (
|
|
150
|
-
<button
|
|
151
|
-
onClick={() => handleNavigate("/admin")}
|
|
152
|
-
disabled={navigating}
|
|
153
|
-
className={buttonClass}
|
|
154
|
-
>
|
|
155
|
-
{navigating && <LoadingSpinner />}
|
|
156
|
-
{navigating ? "Loading..." : "Go to Admin Panel"}
|
|
157
|
-
</button>
|
|
158
|
-
) : bootstrapped ? (
|
|
159
|
-
<button
|
|
160
|
-
onClick={() => handleNavigate("/auth/sign-in")}
|
|
161
|
-
disabled={navigating}
|
|
162
|
-
className={buttonClass}
|
|
163
|
-
>
|
|
164
|
-
{navigating && <LoadingSpinner />}
|
|
165
|
-
{navigating ? "Loading..." : "Sign In"}
|
|
166
|
-
</button>
|
|
167
|
-
) : (
|
|
168
|
-
<button
|
|
169
|
-
onClick={() => handleNavigate("/auth/sign-up")}
|
|
170
|
-
disabled={navigating}
|
|
171
|
-
className={buttonClass}
|
|
172
|
-
>
|
|
173
|
-
{navigating && <LoadingSpinner />}
|
|
174
|
-
{navigating ? "Loading..." : "Create Admin Account"}
|
|
175
|
-
</button>
|
|
176
|
-
)}
|
|
177
|
-
</div>
|
|
178
|
-
|
|
179
|
-
{promoting && (
|
|
180
|
-
<p className="text-sm text-muted-foreground animate-pulse">
|
|
181
|
-
Setting up your admin account...
|
|
182
|
-
</p>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
)
|
|
186
|
-
}
|
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
import { api } from "@convex/_generated/api"
|
|
2
1
|
import { fetchQuery } from "convex/nextjs"
|
|
3
2
|
|
|
3
|
+
import { api } from "@convex/_generated/api"
|
|
4
|
+
|
|
4
5
|
import { generatePageMetadata } from "~/lib/metadata"
|
|
5
|
-
import { normalizeSlug } from "~/lib/utils"
|
|
6
6
|
import { PageContent } from "../PageContent"
|
|
7
7
|
|
|
8
8
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
9
9
|
const { slug } = await params
|
|
10
|
-
return generatePageMetadata({ slug:
|
|
10
|
+
return generatePageMetadata({ slug: slug && slug.length > 0 ? slug : "home" })
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export default async function PublicPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
14
14
|
const { slug } = await params
|
|
15
|
-
const normalized =
|
|
15
|
+
const normalized = slug && slug.length > 0 ? slug : "home"
|
|
16
16
|
|
|
17
|
-
let initialData:
|
|
17
|
+
let initialData: Record<string, unknown>[] | undefined
|
|
18
18
|
try {
|
|
19
|
-
initialData = await fetchQuery(api.pages.getBySlug, {
|
|
20
|
-
slug: normalized,
|
|
21
|
-
_vexDrafts: false,
|
|
22
|
-
})
|
|
19
|
+
initialData = await fetchQuery(api.pages.getBySlug, { slug: normalized })
|
|
23
20
|
} catch {
|
|
24
21
|
// Fall back to client-only fetch
|
|
25
22
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
1
3
|
import { fetchQuery } from "convex/nextjs"
|
|
2
4
|
|
|
3
5
|
import { api } from "@convex/_generated/api"
|
|
@@ -10,8 +12,8 @@ export default async function FrontendLayout({
|
|
|
10
12
|
auth,
|
|
11
13
|
children,
|
|
12
14
|
}: Readonly<{
|
|
13
|
-
auth:
|
|
14
|
-
children:
|
|
15
|
+
auth: ReactNode
|
|
16
|
+
children: ReactNode
|
|
15
17
|
}>) {
|
|
16
18
|
let headerData: Record<string, unknown> | null = null
|
|
17
19
|
let footerData: Record<string, unknown> | null = null
|
|
@@ -10,12 +10,9 @@ export async function generateMetadata() {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export default async function HomePage() {
|
|
13
|
-
let initialData: Record<string, unknown> |
|
|
13
|
+
let initialData: Record<string, unknown>[] | undefined
|
|
14
14
|
try {
|
|
15
|
-
initialData = await fetchQuery(api.pages.getBySlug, {
|
|
16
|
-
slug: "home",
|
|
17
|
-
_vexDrafts: false,
|
|
18
|
-
})
|
|
15
|
+
initialData = await fetchQuery(api.pages.getBySlug, { slug: "home" })
|
|
19
16
|
} catch {
|
|
20
17
|
// Fall back to client-only fetch
|
|
21
18
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { RenderBlocks } from "@vexcms/
|
|
3
|
+
import { RenderBlocks, type RenderableBlock } from "@vexcms/react"
|
|
4
4
|
import { convexQuery } from "@convex-dev/react-query"
|
|
5
5
|
import { useQuery } from "@tanstack/react-query"
|
|
6
6
|
import { anyApi } from "convex/server"
|
|
7
7
|
|
|
8
8
|
import { blockComponents } from "~/vexcms/blocks"
|
|
9
9
|
|
|
10
|
+
type PageBlockLike = RenderableBlock
|
|
11
|
+
|
|
10
12
|
export function SiteFooter({
|
|
11
13
|
initialData,
|
|
12
14
|
}: {
|
|
@@ -17,11 +19,12 @@ export function SiteFooter({
|
|
|
17
19
|
initialData: initialData ?? undefined,
|
|
18
20
|
})
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
const content = footer?.content as PageBlockLike[] | null | undefined
|
|
23
|
+
if (!content) return null
|
|
21
24
|
|
|
22
25
|
return (
|
|
23
26
|
<RenderBlocks
|
|
24
|
-
blocks={
|
|
27
|
+
blocks={content}
|
|
25
28
|
components={blockComponents}
|
|
26
29
|
/>
|
|
27
30
|
)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { RenderBlocks } from "@vexcms/
|
|
3
|
+
import { RenderBlocks, type RenderableBlock } from "@vexcms/react"
|
|
4
4
|
import { convexQuery } from "@convex-dev/react-query"
|
|
5
5
|
import { useQuery } from "@tanstack/react-query"
|
|
6
6
|
import { anyApi } from "convex/server"
|
|
7
7
|
|
|
8
8
|
import { blockComponents } from "~/vexcms/blocks"
|
|
9
9
|
|
|
10
|
+
type PageBlockLike = RenderableBlock
|
|
11
|
+
|
|
10
12
|
export function SiteHeader({
|
|
11
13
|
initialData,
|
|
12
14
|
}: {
|
|
@@ -17,11 +19,12 @@ export function SiteHeader({
|
|
|
17
19
|
initialData: initialData ?? undefined,
|
|
18
20
|
})
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
const content = header?.content as PageBlockLike[] | null | undefined
|
|
23
|
+
if (!content) return null
|
|
21
24
|
|
|
22
25
|
return (
|
|
23
26
|
<RenderBlocks
|
|
24
|
-
blocks={
|
|
27
|
+
blocks={content}
|
|
25
28
|
components={blockComponents}
|
|
26
29
|
/>
|
|
27
30
|
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { api } from "@convex/_generated/api"
|
|
4
|
+
import { useQuery } from "convex/react"
|
|
5
|
+
import { useEffect } from "react"
|
|
6
|
+
|
|
7
|
+
import { buildThemeCss, type ThemeScope } from "@vexcms/core"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Client companion to the server `<ThemeStyle />`: keeps the applied theme
|
|
11
|
+
* live without a page reload.
|
|
12
|
+
*
|
|
13
|
+
* `useQuery` is a Convex subscription — the deployment pushes every change to
|
|
14
|
+
* the active theme document (or to which theme is active), so saving in the
|
|
15
|
+
* admin re-skins every open tab immediately. No polling, no preview channel,
|
|
16
|
+
* no drafts machinery.
|
|
17
|
+
*
|
|
18
|
+
* The CSS goes into an effect-managed `<style>` tag appended to `<head>`
|
|
19
|
+
* rather than a React-hoisted one: hoisted styles are deduplicated by `href`
|
|
20
|
+
* and are not guaranteed to update in place when their text changes, and an
|
|
21
|
+
* appended tag deterministically follows the server-rendered block — equal
|
|
22
|
+
* specificity, later in the document, so it wins the moment it exists. The
|
|
23
|
+
* first push matches the server CSS byte-for-byte, so nothing visibly changes
|
|
24
|
+
* until a real edit lands.
|
|
25
|
+
*
|
|
26
|
+
* Distinguishes loading from empty: while the query is unresolved
|
|
27
|
+
* (`undefined`) the server CSS stands; once it resolves to `null` (no active
|
|
28
|
+
* theme) the override is cleared and `globals.css` shows through.
|
|
29
|
+
*
|
|
30
|
+
* @param props - Input props.
|
|
31
|
+
* @param props.scope - Same contract as `<ThemeStyle />`: `"site"` emits
|
|
32
|
+
* `:root`, `"admin"` emits `:root:root` with the `adminTheme` fallback.
|
|
33
|
+
* @returns Nothing — the style element is managed imperatively.
|
|
34
|
+
*/
|
|
35
|
+
export function ThemeLive(props: { scope?: ThemeScope }) {
|
|
36
|
+
const scope = props.scope ?? "site"
|
|
37
|
+
const theme = useQuery(scope === "admin" ? api.theme.getAdmin : api.theme.getActive)
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (theme === undefined) {return} // still loading — leave the server CSS alone
|
|
41
|
+
|
|
42
|
+
const id = `vex-theme-live-${scope}`
|
|
43
|
+
let el = document.getElementById(id) as HTMLStyleElement | null
|
|
44
|
+
if (!el) {
|
|
45
|
+
el = document.createElement("style")
|
|
46
|
+
el.id = id
|
|
47
|
+
document.head.appendChild(el)
|
|
48
|
+
}
|
|
49
|
+
el.textContent = theme ? buildThemeCss({ theme, scope }) : ""
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
// Admin routes unmount their scope on navigation back to the site; the
|
|
53
|
+
// override must not outlive the layout that owns it.
|
|
54
|
+
el?.remove()
|
|
55
|
+
}
|
|
56
|
+
}, [theme, scope])
|
|
57
|
+
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
@@ -1,104 +1,60 @@
|
|
|
1
|
-
import { fetchQuery } from "convex/nextjs"
|
|
2
|
-
|
|
3
1
|
import { api } from "@convex/_generated/api"
|
|
2
|
+
import { fetchQuery } from "convex/nextjs"
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
* CSS variable name mapping from camelCase field names to CSS custom property names.
|
|
7
|
-
*/
|
|
8
|
-
const FIELD_TO_CSS_VAR: Record<string, string> = {
|
|
9
|
-
background: "background",
|
|
10
|
-
foreground: "foreground",
|
|
11
|
-
card: "card",
|
|
12
|
-
cardForeground: "card-foreground",
|
|
13
|
-
popover: "popover",
|
|
14
|
-
popoverForeground: "popover-foreground",
|
|
15
|
-
primary: "primary",
|
|
16
|
-
primaryForeground: "primary-foreground",
|
|
17
|
-
secondary: "secondary",
|
|
18
|
-
secondaryForeground: "secondary-foreground",
|
|
19
|
-
muted: "muted",
|
|
20
|
-
mutedForeground: "muted-foreground",
|
|
21
|
-
accent: "accent",
|
|
22
|
-
accentForeground: "accent-foreground",
|
|
23
|
-
destructive: "destructive",
|
|
24
|
-
destructiveForeground: "destructive-foreground",
|
|
25
|
-
border: "border",
|
|
26
|
-
input: "input",
|
|
27
|
-
ring: "ring",
|
|
28
|
-
chart1: "chart-1",
|
|
29
|
-
chart2: "chart-2",
|
|
30
|
-
chart3: "chart-3",
|
|
31
|
-
chart4: "chart-4",
|
|
32
|
-
chart5: "chart-5",
|
|
33
|
-
sidebar: "sidebar",
|
|
34
|
-
sidebarForeground: "sidebar-foreground",
|
|
35
|
-
sidebarPrimary: "sidebar-primary",
|
|
36
|
-
sidebarPrimaryForeground: "sidebar-primary-foreground",
|
|
37
|
-
sidebarAccent: "sidebar-accent",
|
|
38
|
-
sidebarAccentForeground: "sidebar-accent-foreground",
|
|
39
|
-
sidebarBorder: "sidebar-border",
|
|
40
|
-
sidebarRing: "sidebar-ring",
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function buildCSSVars(colors: Record<string, unknown> | null | undefined): string {
|
|
44
|
-
if (!colors) return ""
|
|
45
|
-
|
|
46
|
-
const vars: string[] = []
|
|
47
|
-
for (const [fieldName, cssVar] of Object.entries(FIELD_TO_CSS_VAR)) {
|
|
48
|
-
const value = colors[fieldName]
|
|
49
|
-
if (typeof value === "string" && value) {
|
|
50
|
-
vars.push(` --${cssVar}: ${value};`)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return vars.join("\n")
|
|
54
|
-
}
|
|
4
|
+
import { buildThemeCss, type ThemeScope } from "@vexcms/core"
|
|
55
5
|
|
|
56
6
|
/**
|
|
57
|
-
* Server component that
|
|
58
|
-
*
|
|
59
|
-
*
|
|
7
|
+
* Server component that inlines a theme's CSS custom properties.
|
|
8
|
+
*
|
|
9
|
+
* Rendered twice per admin request and once per public request:
|
|
10
|
+
*
|
|
11
|
+
* - the root layout emits the **site** theme at `:root`;
|
|
12
|
+
* - the admin layout emits the **admin** theme at `:root:root`.
|
|
60
13
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
14
|
+
* `:root:root` is specificity (0,2,0) against `:root`'s (0,1,0), so the admin
|
|
15
|
+
* block wins wherever both are present without depending on style-injection
|
|
16
|
+
* order. On public routes the admin layout never renders, so there is exactly
|
|
17
|
+
* one block. Leave `siteSettings.adminTheme` empty and `getAdmin` falls back to
|
|
18
|
+
* the site theme, which is the default: **the admin adopts the site's palette.**
|
|
19
|
+
*
|
|
20
|
+
* Values are written through verbatim. A `color()` field storing
|
|
21
|
+
* `oklch(60.5% 0.175 42)` needs no conversion, because that is already the
|
|
22
|
+
* notation `globals.css` declares its tokens in.
|
|
23
|
+
*
|
|
24
|
+
* This component covers the **first paint** only — no flash of unthemed
|
|
25
|
+
* content. Live updates after a save are `<ThemeLive />`'s job, which
|
|
26
|
+
* subscribes to the same query client-side and overrides this block.
|
|
27
|
+
*
|
|
28
|
+
* Renders nothing when Convex is unreachable (e.g. a build with no deployment)
|
|
29
|
+
* or no theme is active — the app then uses `globals.css` unchanged.
|
|
30
|
+
*
|
|
31
|
+
* @param props - Input props.
|
|
32
|
+
* @param props.scope - `"site"` emits `:root`; `"admin"` emits `:root:root` and
|
|
33
|
+
* reads `adminTheme` with a fallback to the site theme.
|
|
34
|
+
* @returns A `<style>` element, or `null`.
|
|
64
35
|
*/
|
|
65
|
-
export async function ThemeStyle(
|
|
66
|
-
|
|
36
|
+
export async function ThemeStyle(props: { scope?: ThemeScope }) {
|
|
37
|
+
const scope = props.scope ?? "site"
|
|
38
|
+
|
|
39
|
+
let theme: null | Record<string, unknown> = null
|
|
67
40
|
try {
|
|
68
|
-
theme =
|
|
69
|
-
? await fetchQuery(api.theme.getActivePreview) as Record<string, unknown> | null
|
|
70
|
-
: await fetchQuery(api.theme.getActive) as Record<string, unknown> | null
|
|
41
|
+
theme = await fetchQuery(scope === "admin" ? api.theme.getAdmin : api.theme.getActive)
|
|
71
42
|
} catch {
|
|
72
|
-
//
|
|
43
|
+
// No deployment reachable at build time — fall back to globals.css.
|
|
73
44
|
return null
|
|
74
45
|
}
|
|
46
|
+
if (!theme) {return null}
|
|
75
47
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const lightColors = theme.light as Record<string, unknown> | undefined
|
|
79
|
-
const darkColors = theme.dark as Record<string, unknown> | undefined
|
|
80
|
-
|
|
81
|
-
const lightVars = buildCSSVars(lightColors)
|
|
82
|
-
const darkVars = buildCSSVars(darkColors)
|
|
83
|
-
|
|
84
|
-
if (!lightVars && !darkVars) return null
|
|
85
|
-
|
|
86
|
-
const css = [
|
|
87
|
-
lightVars ? `:root {\n${lightVars}\n}` : "",
|
|
88
|
-
darkVars ? `.dark {\n${darkVars}\n}` : "",
|
|
89
|
-
]
|
|
90
|
-
.filter(Boolean)
|
|
91
|
-
.join("\n\n")
|
|
92
|
-
|
|
93
|
-
const radius = theme.radius as string | undefined
|
|
94
|
-
const fontFamily = theme.fontFamily as string | undefined
|
|
95
|
-
const extraVars: string[] = []
|
|
96
|
-
if (radius) extraVars.push(` --radius: ${radius};`)
|
|
97
|
-
if (fontFamily) extraVars.push(` --font-sans: ${fontFamily};`)
|
|
98
|
-
|
|
99
|
-
const extraCSS = extraVars.length > 0 ? `\n:root {\n${extraVars.join("\n")}\n}` : ""
|
|
48
|
+
const css = buildThemeCss({ theme, scope })
|
|
49
|
+
if (!css) {return null}
|
|
100
50
|
|
|
51
|
+
// `precedence` opts into React 19 style hoisting, so this lands in <head>
|
|
52
|
+
// before first paint instead of mid-body.
|
|
101
53
|
return (
|
|
102
|
-
<style
|
|
54
|
+
<style
|
|
55
|
+
dangerouslySetInnerHTML={{ __html: css }}
|
|
56
|
+
href={`vex-theme-${scope}`}
|
|
57
|
+
precedence="high"
|
|
58
|
+
/>
|
|
103
59
|
)
|
|
104
60
|
}
|