create-vexcms 0.0.19 → 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.
Files changed (154) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +93 -83
  3. package/dist/index.js +246 -81
  4. package/dist/index.mjs +30976 -0
  5. package/package.json +35 -15
  6. package/templates/base-nextjs/README.md +53 -205
  7. package/templates/base-nextjs/_env.example +7 -9
  8. package/templates/base-nextjs/_gitignore +41 -1
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
  10. package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
  11. package/templates/base-nextjs/convex/_generated/server.js +8 -0
  12. package/templates/base-nextjs/convex/auth/api.ts +49 -3
  13. package/templates/base-nextjs/convex/auth/db.ts +42 -300
  14. package/templates/base-nextjs/convex/auth/index.ts +11 -7
  15. package/templates/base-nextjs/convex/auth/options.ts +5 -29
  16. package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
  17. package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
  18. package/templates/base-nextjs/convex/http.ts +18 -3
  19. package/templates/base-nextjs/convex/schema.ts +104 -2
  20. package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
  21. package/templates/base-nextjs/convex/vex/globals.ts +19 -82
  22. package/templates/base-nextjs/convex/vex/media.ts +19 -246
  23. package/templates/base-nextjs/convex/vex.schema.ts +138 -0
  24. package/templates/base-nextjs/convex/vex.ts +19 -0
  25. package/templates/base-nextjs/convex.json +6 -0
  26. package/templates/base-nextjs/eslint.config.mjs +1 -21
  27. package/templates/base-nextjs/next.config.ts +10 -0
  28. package/templates/base-nextjs/package.json +47 -48
  29. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
  30. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
  31. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
  32. package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
  33. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
  34. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
  35. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
  36. package/templates/base-nextjs/src/app/globals.css +240 -104
  37. package/templates/base-nextjs/src/app/layout.tsx +23 -13
  38. package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
  39. package/templates/base-nextjs/src/auth/access.ts +46 -0
  40. package/templates/base-nextjs/src/auth/client.tsx +17 -5
  41. package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
  42. package/templates/base-nextjs/src/auth/serverUtils.ts +15 -22
  43. package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
  44. package/templates/base-nextjs/src/components/component-example.tsx +56 -464
  45. package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
  46. package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
  47. package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
  48. package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
  49. package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
  50. package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
  51. package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
  52. package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
  53. package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
  54. package/templates/base-nextjs/src/db/constants/index.ts +22 -35
  55. package/templates/base-nextjs/src/lib/utils.ts +0 -12
  56. package/templates/base-nextjs/src/proxy.ts +98 -11
  57. package/templates/base-nextjs/src/vex.config.ts +35 -0
  58. package/templates/base-nextjs/src/vex.types.ts +293 -0
  59. package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
  60. package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
  61. package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
  62. package/templates/base-nextjs/tsconfig.json +3 -0
  63. package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
  64. package/templates/marketing-site/convex/_generated/api.js +23 -0
  65. package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
  66. package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
  67. package/templates/marketing-site/convex/_generated/server.js +101 -0
  68. package/templates/marketing-site/convex/footers.ts +11 -7
  69. package/templates/marketing-site/convex/headers.ts +15 -5
  70. package/templates/marketing-site/convex/pages.ts +20 -35
  71. package/templates/marketing-site/convex/schema.ts +119 -0
  72. package/templates/marketing-site/convex/seed.ts +582 -35
  73. package/templates/marketing-site/convex/siteSettings.ts +19 -5
  74. package/templates/marketing-site/convex/theme.ts +55 -61
  75. package/templates/marketing-site/convex/vex.schema.ts +355 -0
  76. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
  77. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
  78. package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
  79. package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
  80. package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
  81. package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
  82. package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
  83. package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
  84. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +2 -2
  85. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  86. package/templates/marketing-site/src/vex.config.ts +35 -0
  87. package/templates/marketing-site/src/vex.types.ts +634 -0
  88. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  90. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  92. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  93. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  94. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  96. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  97. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  98. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  100. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  102. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  104. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  105. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  106. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  107. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  108. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  110. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  111. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  112. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  113. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  114. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  115. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  116. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  117. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  118. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  119. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  120. package/templates/base-nextjs/convex/vex/collections.ts +0 -321
  121. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  122. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  123. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  124. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  125. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  126. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  127. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  128. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  129. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  130. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  131. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  132. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  133. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  134. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  135. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  136. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  137. package/templates/base-nextjs/src/db/types.ts +0 -6
  138. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  139. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  140. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  141. package/templates/base-nextjs/vex.config.ts +0 -25
  142. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  144. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  145. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  146. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  147. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  148. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  149. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  150. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  151. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  152. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  153. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  154. package/templates/marketing-site/vex.config.ts +0 -32
@@ -1,101 +0,0 @@
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
- }
@@ -1,15 +0,0 @@
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
- }
@@ -1,51 +0,0 @@
1
- import { SidebarTrigger } from "@vexcms/ui"
2
- import { checkAdminAccess, sanitizeConfigForClient } from "@vexcms/core"
3
- import { redirect } from "next/navigation"
4
-
5
- import config from "~/../vex.config"
6
- import { getCurrentUser } from "~/auth/serverUtils"
7
- import { access } from "~/vexcms/access"
8
- import { AdminLayoutWrapper } from "./AdminLayoutWrapper"
9
-
10
- const clientConfig = sanitizeConfigForClient(config)
11
-
12
- export default async function AdminLayout({ children }: { children: React.ReactNode }) {
13
- const user = await getCurrentUser()
14
-
15
- // No user — redirect to sign in
16
- if (!user) {
17
- redirect("/auth/sign-in")
18
- }
19
-
20
- const userRoles = Array.isArray(user.role) ? user.role : [user.role].filter(Boolean)
21
-
22
- // Check admin panel access
23
- const hasAccess = checkAdminAccess({
24
- access,
25
- user: user as Record<string, any>,
26
- userRoles,
27
- })
28
-
29
- if (!hasAccess) {
30
- redirect("/?error=unauthorized")
31
- }
32
-
33
- const permissionUser = {
34
- id: user.id,
35
- roles: userRoles,
36
- name: user.name,
37
- email: user.email,
38
- avatar: user.image,
39
- }
40
-
41
- return (
42
- <AdminLayoutWrapper
43
- config={clientConfig}
44
- user={{ name: user.name, avatar: user.image, email: user.email }}
45
- permissionUser={permissionUser}
46
- >
47
- <SidebarTrigger />
48
- <div className="flex flex-col flex-1 min-h-0 overflow-hidden">{children}</div>
49
- </AdminLayoutWrapper>
50
- )
51
- }
@@ -1,11 +0,0 @@
1
- "use client"
2
-
3
- import { ThemeProvider as NextThemesProvider } from "next-themes"
4
- import * as React from "react"
5
-
6
- export function ThemeProvider({
7
- children,
8
- ...props
9
- }: React.ComponentProps<typeof NextThemesProvider>) {
10
- return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11
- }
@@ -1,67 +0,0 @@
1
- "use client"
2
-
3
- import { Moon, Sun } from "lucide-react"
4
- import { useTheme } from "next-themes"
5
- import { type ComponentPropsWithRef } from "react"
6
-
7
- import { Button } from "~/components/ui/button"
8
- import {
9
- DropdownMenu,
10
- DropdownMenuCheckboxItem,
11
- DropdownMenuContent,
12
- DropdownMenuTrigger,
13
- } from "~/components/ui/dropdown-menu"
14
-
15
- export function ThemeToggle({ ...divProps }: ComponentPropsWithRef<"div">) {
16
- const { resolvedTheme, setTheme, theme } = useTheme()
17
-
18
- return (
19
- <div {...divProps}>
20
- <DropdownMenu>
21
- <DropdownMenuTrigger
22
- render={<Button size="icon" variant="outline" />}
23
- suppressHydrationWarning
24
- >
25
- <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
26
- <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
27
- <span className="sr-only">Toggle theme</span>
28
- </DropdownMenuTrigger>
29
- <DropdownMenuContent>
30
- <DropdownMenuCheckboxItem
31
- checked={Boolean(theme && theme === "light")}
32
- disabled={Boolean(theme && theme === "light")}
33
- onCheckedChange={(val) => {
34
- if (val) {
35
- setTheme("light")
36
- }
37
- }}
38
- >
39
- Light
40
- </DropdownMenuCheckboxItem>
41
- <DropdownMenuCheckboxItem
42
- checked={Boolean(theme && theme === "dark")}
43
- disabled={Boolean(theme && theme === "dark")}
44
- onCheckedChange={(val) => {
45
- if (val) {
46
- setTheme("dark")
47
- }
48
- }}
49
- >
50
- Dark
51
- </DropdownMenuCheckboxItem>
52
- <DropdownMenuCheckboxItem
53
- checked={Boolean(theme && theme === "system")}
54
- disabled={Boolean(theme && theme === "system")}
55
- onCheckedChange={(val) => {
56
- if (val) {
57
- setTheme("system")
58
- }
59
- }}
60
- >
61
- System
62
- </DropdownMenuCheckboxItem>
63
- </DropdownMenuContent>
64
- </DropdownMenu>
65
- </div>
66
- )
67
- }
@@ -1,6 +0,0 @@
1
- import { type Doc, type Id } from "@convex/_generated/dataModel"
2
-
3
- import { type TABLE_SLUG_USERS } from "./constants"
4
-
5
- export type User = Doc<typeof TABLE_SLUG_USERS>
6
- export type UserID = Id<typeof TABLE_SLUG_USERS>
@@ -1,44 +0,0 @@
1
- import { defineAccess } from "@vexcms/core"
2
-
3
- import { USER_ROLES } from "~/db/constants"
4
- import { users } from "~/vexcms/collections/users"
5
-
6
- /**
7
- * Access control configuration for the admin panel.
8
- *
9
- * - `admin` on each role controls whether that role can access the admin panel.
10
- * - Resource permissions control CRUD access per collection.
11
- * - Add your own collections to `resources` for autocomplete in `permissions`.
12
- *
13
- * @see https://vexcms.dev/docs/access-control
14
- */
15
- export const access = defineAccess({
16
- roles: [USER_ROLES.admin, USER_ROLES.editor, USER_ROLES.user],
17
- adminRoles: [USER_ROLES.admin],
18
- resources: [users],
19
- userCollection: users,
20
- permissions: {
21
- admin: {
22
- admin: true,
23
- user: true,
24
- },
25
- editor: {
26
- admin: true,
27
- user: {
28
- create: false,
29
- delete: false,
30
- read: true,
31
- update: false,
32
- },
33
- },
34
- user: {
35
- admin: false,
36
- user: {
37
- create: false,
38
- delete: false,
39
- read: ({ data, user }) => data._id === user._id,
40
- update: ({ data, user }) => data._id === user._id,
41
- },
42
- },
43
- },
44
- })
@@ -1,4 +0,0 @@
1
- import { betterAuthOptions } from "@convex/auth/options"
2
- import { vexBetterAuth } from "@vexcms/better-auth"
3
-
4
- export const auth = vexBetterAuth({ config: betterAuthOptions })
@@ -1,15 +0,0 @@
1
- import { defineMediaCollection } from "@vexcms/core"
2
-
3
- import { TABLE_SLUG_MEDIA } from "~/db/constants"
4
-
5
- export const media = defineMediaCollection({
6
- slug: TABLE_SLUG_MEDIA,
7
- admin: {
8
- group: "Media",
9
- useAsTitle: "filename",
10
- },
11
- labels: {
12
- plural: "Media",
13
- singular: "Media",
14
- },
15
- })
@@ -1,25 +0,0 @@
1
- import { defineConfig } from "@vexcms/core"
2
- import { convexFileStorage } from "@vexcms/file-storage-convex"
3
-
4
- import { auth } from "~/vexcms/auth"
5
- import { access } from "~/vexcms/access"
6
-
7
- export default defineConfig({
8
- access,
9
- admin: {
10
- meta: {
11
- titleSuffix: " | Admin",
12
- },
13
- sidebar: {
14
- hideGlobals: true,
15
- },
16
- user: "user",
17
- },
18
- auth,
19
- basePath: "/admin",
20
- collections: [],
21
- media: {
22
- collections: [],
23
- storageAdapter: convexFileStorage(),
24
- },
25
- })
@@ -1,55 +0,0 @@
1
- "use client"
2
-
3
- import { RenderBlocks, useVexPreview } from "@vexcms/ui"
4
- import { convexQuery } from "@convex-dev/react-query"
5
- import { useQuery } from "@tanstack/react-query"
6
- import { anyApi } from "convex/server"
7
-
8
- import { blockComponents } from "~/vexcms/blocks"
9
-
10
- /**
11
- * Client component for live preview using TanStack Query.
12
- * initialData from server fetch eliminates loading flash.
13
- * The reactive subscription picks up draft snapshot changes in real-time.
14
- */
15
- export function PreviewPageContent({
16
- slug,
17
- initialData,
18
- }: {
19
- slug: string
20
- initialData?: Record<string, unknown> | null
21
- }) {
22
- const { data: page } = useQuery({
23
- ...convexQuery(anyApi.pages.getBySlug, {
24
- slug,
25
- _vexDrafts: "snapshot",
26
- }),
27
- initialData: initialData ?? undefined,
28
- })
29
-
30
- // Notify admin panel's live preview when data changes
31
- useVexPreview({ data: page })
32
-
33
- if (page === null || page === undefined) {
34
- return (
35
- <div className="mx-auto max-w-3xl px-4 py-12">
36
- <h1 className="text-2xl font-bold">Preview not found</h1>
37
- <p className="mt-2 text-muted-foreground">
38
- No page with slug &ldquo;{slug}&rdquo; exists.
39
- </p>
40
- </div>
41
- )
42
- }
43
-
44
- return (
45
- <>
46
- {/* Preview banner */}
47
- <div className="fixed top-0 left-0 right-0 z-50 border-b border-yellow-200 bg-yellow-50 px-4 py-2 text-center text-sm text-yellow-800">
48
- Preview Mode — This page may not be published yet.
49
- </div>
50
- <div className="pt-10">
51
- <RenderBlocks blocks={page.content} components={blockComponents} />
52
- </div>
53
- </>
54
- )
55
- }
@@ -1,27 +0,0 @@
1
- import { fetchQuery } from "convex/nextjs"
2
-
3
- import { api } from "@convex/_generated/api"
4
-
5
- import { normalizeSlug } from "~/lib/utils"
6
- import { PreviewPageContent } from "./PreviewPageContent"
7
-
8
- export default async function PreviewPage({
9
- params,
10
- }: {
11
- params: Promise<{ slug: string }>
12
- }) {
13
- const { slug } = await params
14
- const normalized = normalizeSlug(slug)
15
-
16
- let initialData: Record<string, unknown> | null = null
17
- try {
18
- initialData = await fetchQuery(api.pages.getBySlug, {
19
- slug: normalized,
20
- _vexDrafts: "snapshot" as any,
21
- })
22
- } catch {
23
- // Fall back to client-only fetch
24
- }
25
-
26
- return <PreviewPageContent slug={normalized} initialData={initialData} />
27
- }
@@ -1,18 +0,0 @@
1
- import { ThemeInjector } from "~/components/ThemeInjector"
2
- import { ThemeStyle } from "~/components/ThemeStyle"
3
-
4
- export default function PreviewLayout({
5
- children,
6
- }: Readonly<{
7
- children: React.ReactNode
8
- }>) {
9
- return (
10
- <>
11
- {/* Server-rendered draft theme CSS — eliminates flash on preview load */}
12
- <ThemeStyle drafts />
13
- {/* Client-side theme injector — picks up real-time draft changes via snapshot */}
14
- <ThemeInjector siteSettingsSlug="site_settings" drafts="snapshot" />
15
- {children}
16
- </>
17
- )
18
- }
@@ -1,71 +0,0 @@
1
- "use client"
2
-
3
- import { useCallback } from "react"
4
- import { useMutation, useQuery } from "convex/react"
5
- import { useRouter } from "next/navigation"
6
- import { AdminLayout, OnboardingTour, useOnboardingTour } from "@vexcms/admin-next"
7
- import type { PermissionUser } from "@vexcms/admin-next"
8
- import type { ClientVexConfig } from "@vexcms/core"
9
- import { api } from "@convex/_generated/api"
10
- import { access } from "~/vexcms/access"
11
-
12
- /**
13
- * Client wrapper that provides the access config directly (not through RSC serialization).
14
- * Includes the onboarding tour for the marketing site template.
15
- */
16
- export function AdminLayoutWrapper({
17
- config,
18
- user,
19
- permissionUser,
20
- children,
21
- }: {
22
- config: ClientVexConfig
23
- user?: { name: string; email: string; avatar?: string }
24
- permissionUser?: PermissionUser
25
- children: React.ReactNode
26
- }) {
27
- const router = useRouter()
28
- const onboardingStatus = useQuery(api.vex.firstUser.getOnboardingStatus)
29
- const completeOnboarding = useMutation(api.vex.firstUser.completeOnboarding)
30
- const resetOnboarding = useMutation(api.vex.firstUser.resetOnboarding)
31
-
32
- const handleComplete = useCallback(() => {
33
- completeOnboarding()
34
- }, [completeOnboarding])
35
-
36
- const handleReset = useCallback(() => {
37
- resetOnboarding()
38
- }, [resetOnboarding])
39
-
40
- const navigate = useCallback((path: string) => {
41
- router.push(path)
42
- }, [router])
43
-
44
- const { showTour, completeTour } = useOnboardingTour({
45
- config,
46
- isComplete: onboardingStatus?.complete ?? true,
47
- onComplete: handleComplete,
48
- onReset: handleReset,
49
- })
50
-
51
- // Use the first collection as the example for the tour
52
- const exampleCollection = config.collections[0]?.slug
53
-
54
- return (
55
- <AdminLayout
56
- config={config}
57
- user={user}
58
- permissionUser={permissionUser}
59
- access={access}
60
- >
61
- {children}
62
- <OnboardingTour
63
- active={showTour}
64
- onComplete={completeTour}
65
- basePath={config.basePath}
66
- exampleCollection={exampleCollection}
67
- navigate={navigate}
68
- />
69
- </AdminLayout>
70
- )
71
- }
@@ -1,128 +0,0 @@
1
- "use client"
2
-
3
- import { useQuery } from "convex/react"
4
- import { useVexPreview } from "@vexcms/ui"
5
- import { anyApi } from "convex/server"
6
-
7
- /**
8
- * CSS variable name mapping from camelCase field names to CSS custom property names.
9
- */
10
- const FIELD_TO_CSS_VAR: Record<string, string> = {
11
- background: "background",
12
- foreground: "foreground",
13
- card: "card",
14
- cardForeground: "card-foreground",
15
- popover: "popover",
16
- popoverForeground: "popover-foreground",
17
- primary: "primary",
18
- primaryForeground: "primary-foreground",
19
- secondary: "secondary",
20
- secondaryForeground: "secondary-foreground",
21
- muted: "muted",
22
- mutedForeground: "muted-foreground",
23
- accent: "accent",
24
- accentForeground: "accent-foreground",
25
- destructive: "destructive",
26
- destructiveForeground: "destructive-foreground",
27
- border: "border",
28
- input: "input",
29
- ring: "ring",
30
- chart1: "chart-1",
31
- chart2: "chart-2",
32
- chart3: "chart-3",
33
- chart4: "chart-4",
34
- chart5: "chart-5",
35
- sidebar: "sidebar",
36
- sidebarForeground: "sidebar-foreground",
37
- sidebarPrimary: "sidebar-primary",
38
- sidebarPrimaryForeground: "sidebar-primary-foreground",
39
- sidebarAccent: "sidebar-accent",
40
- sidebarAccentForeground: "sidebar-accent-foreground",
41
- sidebarBorder: "sidebar-border",
42
- sidebarRing: "sidebar-ring",
43
- }
44
-
45
- function buildCSSVars(colors: Record<string, unknown> | null | undefined): string {
46
- if (!colors) return ""
47
-
48
- const vars: string[] = []
49
- for (const [fieldName, cssVar] of Object.entries(FIELD_TO_CSS_VAR)) {
50
- const value = colors[fieldName]
51
- if (typeof value === "string" && value) {
52
- vars.push(` --${cssVar}: ${value};`)
53
- }
54
- }
55
- return vars.join("\n")
56
- }
57
-
58
- /**
59
- * Client component that reads the active theme from the site_settings global
60
- * and injects CSS variables for light and dark modes.
61
- *
62
- * @param props.siteSettingsSlug - The global slug for site settings
63
- * @param props.drafts - When "snapshot", reads draft/preview data instead of published
64
- */
65
- export function ThemeInjector({
66
- siteSettingsSlug,
67
- drafts,
68
- }: {
69
- siteSettingsSlug: string
70
- drafts?: "snapshot" | false
71
- }) {
72
- // Use globals.get which supports draft awareness
73
- const settingsDoc = useQuery(
74
- anyApi.vex.globals?.get,
75
- {
76
- globalSlug: siteSettingsSlug,
77
- ...(drafts ? { _vexDrafts: drafts } : {}),
78
- },
79
- ) as Record<string, unknown> | null | undefined
80
-
81
- const activeThemeId = settingsDoc?.activeTheme as string | undefined
82
-
83
- // Fetch the active theme document
84
- // In draft mode, use versioned query to pick up unpublished theme changes
85
- const theme = useQuery(
86
- drafts
87
- ? anyApi.vex.versions?.getDocumentForEdit
88
- : anyApi.vex.api.themes?.get,
89
- activeThemeId
90
- ? drafts
91
- ? { collectionSlug: "themes", documentId: activeThemeId }
92
- : { id: activeThemeId }
93
- : "skip",
94
- ) as Record<string, unknown> | null | undefined
95
-
96
- // In draft mode, notify the admin panel when settings or theme data changes
97
- // so the live preview spinner stops
98
- useVexPreview({ data: drafts ? { settings: settingsDoc, theme } : undefined })
99
-
100
- if (!theme) return null
101
-
102
- const lightColors = theme.light as Record<string, unknown> | undefined
103
- const darkColors = theme.dark as Record<string, unknown> | undefined
104
-
105
- const lightVars = buildCSSVars(lightColors)
106
- const darkVars = buildCSSVars(darkColors)
107
-
108
- if (!lightVars && !darkVars) return null
109
-
110
- const css = [
111
- lightVars ? `:root {\n${lightVars}\n}` : "",
112
- darkVars ? `.dark {\n${darkVars}\n}` : "",
113
- ]
114
- .filter(Boolean)
115
- .join("\n\n")
116
-
117
- const radius = theme.radius as string | undefined
118
- const fontFamily = theme.fontFamily as string | undefined
119
- const extraVars: string[] = []
120
- if (radius) extraVars.push(` --radius: ${radius};`)
121
- if (fontFamily) extraVars.push(` --font-sans: ${fontFamily};`)
122
-
123
- const extraCSS = extraVars.length > 0 ? `\n:root {\n${extraVars.join("\n")}\n}` : ""
124
-
125
- return (
126
- <style dangerouslySetInnerHTML={{ __html: css + extraCSS }} />
127
- )
128
- }