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.
- 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 -22
- 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 +98 -11
- 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/components/motion-primitives/animated-group.tsx +2 -2
- 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 -321
- 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,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
|
-
}
|
|
Binary file
|
|
@@ -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,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,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
|
-
})
|
|
Binary file
|
|
@@ -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 “{slug}” 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
|
-
}
|