create-vexcms 0.0.7 → 0.0.9
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/package.json +1 -1
- package/templates/base-nextjs/convex/vex/globals.ts +82 -0
- package/templates/base-nextjs/convex/vex/versions.ts +66 -6
- package/templates/base-nextjs/package.json +8 -8
- package/templates/base-nextjs/src/app/admin/layout.tsx +29 -11
- package/templates/base-nextjs/src/db/constants/auth.ts +1 -3
- package/templates/base-nextjs/src/vexcms/access.ts +29 -13
- package/templates/base-nextjs/src/vexcms/collections/users.ts +2 -2
- package/templates/base-nextjs/vex.config.ts +2 -0
- package/templates/marketing-site/convex/footers.ts +14 -0
- package/templates/marketing-site/convex/headers.ts +14 -0
- package/templates/marketing-site/convex/siteSettings.ts +14 -0
- package/templates/marketing-site/convex/theme.ts +75 -0
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +74 -0
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +19 -51
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +37 -0
- package/templates/marketing-site/src/app/(frontend)/page.tsx +24 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +55 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +19 -63
- package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +18 -0
- package/templates/marketing-site/src/components/SiteFooter.tsx +28 -0
- package/templates/marketing-site/src/components/SiteHeader.tsx +28 -0
- package/templates/marketing-site/src/components/ThemeInjector.tsx +128 -0
- package/templates/marketing-site/src/components/ThemeStyle.tsx +104 -0
- package/templates/marketing-site/src/components/admin/IconPickerField.tsx +134 -0
- package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +138 -0
- package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +292 -0
- package/templates/marketing-site/src/lib/metadata.ts +104 -0
- package/templates/marketing-site/src/vexcms/access.ts +28 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +36 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +46 -0
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +70 -0
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +71 -0
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +61 -0
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +73 -0
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +65 -0
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +71 -0
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +65 -0
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +174 -0
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +52 -0
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +152 -0
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +68 -0
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +74 -0
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +154 -0
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +122 -0
- package/templates/marketing-site/src/vexcms/blocks/config.ts +40 -0
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +9 -0
- package/templates/marketing-site/src/vexcms/blocks/index.ts +39 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +11 -8
- package/templates/marketing-site/src/vexcms/collections/headers.ts +10 -14
- package/templates/marketing-site/src/vexcms/collections/index.ts +0 -1
- package/templates/marketing-site/src/vexcms/collections/pages.ts +30 -4
- package/templates/marketing-site/src/vexcms/collections/themes.ts +107 -14
- package/templates/marketing-site/src/vexcms/globals/index.ts +1 -0
- package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +57 -0
- package/templates/marketing-site/vex.config.ts +5 -5
- package/templates/base-nextjs/src/auth/permissions.ts +0 -102
- package/templates/marketing-site/src/vexcms/collections/site_settings.ts +0 -31
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BlockComponentProps } from "@vexcms/ui"
|
|
2
|
+
import type { ComponentType } from "react"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BLOCK_SLUG_CTA,
|
|
6
|
+
BLOCK_SLUG_FAQ,
|
|
7
|
+
BLOCK_SLUG_FEATURES,
|
|
8
|
+
BLOCK_SLUG_FOOTER,
|
|
9
|
+
BLOCK_SLUG_HEADER,
|
|
10
|
+
BLOCK_SLUG_HERO,
|
|
11
|
+
BLOCK_SLUG_HOW_IT_WORKS,
|
|
12
|
+
BLOCK_SLUG_ROADMAP,
|
|
13
|
+
} from "./constants"
|
|
14
|
+
import CTABlock from "./CTA"
|
|
15
|
+
import FAQBlock from "./FAQ"
|
|
16
|
+
import FeaturesBlock from "./Features"
|
|
17
|
+
import FooterBlock from "./Footer"
|
|
18
|
+
import HeaderBlock from "./Header"
|
|
19
|
+
import HeroBlock from "./Hero"
|
|
20
|
+
import HowItWorksBlock from "./HowItWorks"
|
|
21
|
+
import RoadmapBlock from "./Roadmap"
|
|
22
|
+
|
|
23
|
+
/** Block component map for use with RenderBlocks */
|
|
24
|
+
export const blockComponents: Record<
|
|
25
|
+
string,
|
|
26
|
+
ComponentType<BlockComponentProps>
|
|
27
|
+
> = {
|
|
28
|
+
[BLOCK_SLUG_CTA]: CTABlock,
|
|
29
|
+
[BLOCK_SLUG_FAQ]: FAQBlock,
|
|
30
|
+
[BLOCK_SLUG_FEATURES]: FeaturesBlock,
|
|
31
|
+
[BLOCK_SLUG_FOOTER]: FooterBlock,
|
|
32
|
+
[BLOCK_SLUG_HEADER]: HeaderBlock,
|
|
33
|
+
[BLOCK_SLUG_HERO]: HeroBlock,
|
|
34
|
+
[BLOCK_SLUG_HOW_IT_WORKS]: HowItWorksBlock,
|
|
35
|
+
[BLOCK_SLUG_ROADMAP]: RoadmapBlock,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Re-export configs for convenience in client code
|
|
39
|
+
export { allBlocks, footerBlocks, headerBlocks, pageBlocks } from "./config"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { blocks, defineCollection, text } from "@vexcms/core"
|
|
2
2
|
|
|
3
|
-
import { TABLE_SLUG_FOOTERS
|
|
3
|
+
import { TABLE_SLUG_FOOTERS } from "~/db/constants"
|
|
4
|
+
import { footerBlocks } from "~/vexcms/blocks/config"
|
|
4
5
|
|
|
5
6
|
export const footers = defineCollection({
|
|
6
7
|
slug: TABLE_SLUG_FOOTERS,
|
|
@@ -13,12 +14,14 @@ export const footers = defineCollection({
|
|
|
13
14
|
label: "Name",
|
|
14
15
|
required: true,
|
|
15
16
|
}),
|
|
16
|
-
content:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
content: blocks({
|
|
18
|
+
blocks: footerBlocks,
|
|
19
|
+
label: "Footer",
|
|
20
|
+
max: 1,
|
|
21
|
+
labels: {
|
|
22
|
+
singular: "Footer",
|
|
23
|
+
plural: "Footers",
|
|
24
|
+
},
|
|
22
25
|
}),
|
|
23
26
|
},
|
|
24
27
|
labels: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { blocks, defineCollection, text } from "@vexcms/core"
|
|
2
2
|
|
|
3
|
-
import { TABLE_SLUG_HEADERS
|
|
3
|
+
import { TABLE_SLUG_HEADERS } from "~/db/constants"
|
|
4
|
+
import { headerBlocks } from "~/vexcms/blocks/config"
|
|
4
5
|
|
|
5
6
|
export const headers = defineCollection({
|
|
6
7
|
slug: TABLE_SLUG_HEADERS,
|
|
@@ -13,19 +14,14 @@ export const headers = defineCollection({
|
|
|
13
14
|
label: "Name",
|
|
14
15
|
required: true,
|
|
15
16
|
}),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
content: blocks({
|
|
18
|
+
blocks: headerBlocks,
|
|
19
|
+
label: "Header",
|
|
20
|
+
max: 1,
|
|
21
|
+
labels: {
|
|
22
|
+
singular: "Header",
|
|
23
|
+
plural: "Headers",
|
|
22
24
|
},
|
|
23
|
-
label: "Logo URL",
|
|
24
|
-
to: TABLE_SLUG_MEDIA,
|
|
25
|
-
}),
|
|
26
|
-
sticky: checkbox({
|
|
27
|
-
defaultValue: false,
|
|
28
|
-
label: "Sticky Header",
|
|
29
25
|
}),
|
|
30
26
|
},
|
|
31
27
|
labels: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { defineCollection,
|
|
1
|
+
import { blocks, defineCollection, imageUrl, text } from "@vexcms/core"
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { TABLE_SLUG_PAGES } from "~/db/constants"
|
|
4
|
+
import { pageBlocks } from "~/vexcms/blocks/config"
|
|
4
5
|
|
|
5
6
|
export const pages = defineCollection({
|
|
6
7
|
slug: TABLE_SLUG_PAGES,
|
|
@@ -25,9 +26,34 @@ export const pages = defineCollection({
|
|
|
25
26
|
label: "Slug",
|
|
26
27
|
required: true,
|
|
27
28
|
}),
|
|
28
|
-
content:
|
|
29
|
+
content: blocks({
|
|
30
|
+
blocks: pageBlocks,
|
|
29
31
|
label: "Content",
|
|
30
|
-
|
|
32
|
+
labels: {
|
|
33
|
+
singular: "Block",
|
|
34
|
+
plural: "Blocks",
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
metaTitle: text({
|
|
38
|
+
label: "Meta Title",
|
|
39
|
+
admin: {
|
|
40
|
+
description: "Custom <title> tag. Falls back to page title if empty.",
|
|
41
|
+
position: "sidebar",
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
metaDescription: text({
|
|
45
|
+
label: "Meta Description",
|
|
46
|
+
admin: {
|
|
47
|
+
description: "Custom meta description for search results.",
|
|
48
|
+
position: "sidebar",
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
ogImage: imageUrl({
|
|
52
|
+
label: "OG Image",
|
|
53
|
+
admin: {
|
|
54
|
+
description: "Custom Open Graph image URL for social sharing.",
|
|
55
|
+
position: "sidebar",
|
|
56
|
+
},
|
|
31
57
|
}),
|
|
32
58
|
},
|
|
33
59
|
labels: {
|
|
@@ -1,33 +1,126 @@
|
|
|
1
|
-
import { defineCollection, text } from "@vexcms/core"
|
|
1
|
+
import { color, defineCollection, tabs, text, ui } from "@vexcms/core"
|
|
2
2
|
|
|
3
|
+
import ThemeImportField from "~/components/admin/ThemeImportField"
|
|
3
4
|
import { TABLE_SLUG_THEMES } from "~/db/constants"
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Helper to create a set of shadcn/Tailwind color fields for a theme mode.
|
|
8
|
+
*/
|
|
9
|
+
function themeColorFields() {
|
|
10
|
+
return {
|
|
11
|
+
background: color({ label: "Background", defaultValue: "#ffffff" }),
|
|
12
|
+
foreground: color({ label: "Foreground", defaultValue: "#0a0a0a" }),
|
|
13
|
+
card: color({ label: "Card", defaultValue: "#ffffff" }),
|
|
14
|
+
cardForeground: color({ label: "Card Foreground", defaultValue: "#0a0a0a" }),
|
|
15
|
+
popover: color({ label: "Popover", defaultValue: "#ffffff" }),
|
|
16
|
+
popoverForeground: color({ label: "Popover Foreground", defaultValue: "#0a0a0a" }),
|
|
17
|
+
primary: color({ label: "Primary", defaultValue: "#171717" }),
|
|
18
|
+
primaryForeground: color({ label: "Primary Foreground", defaultValue: "#fafafa" }),
|
|
19
|
+
secondary: color({ label: "Secondary", defaultValue: "#f5f5f5" }),
|
|
20
|
+
secondaryForeground: color({ label: "Secondary Foreground", defaultValue: "#171717" }),
|
|
21
|
+
muted: color({ label: "Muted", defaultValue: "#f5f5f5" }),
|
|
22
|
+
mutedForeground: color({ label: "Muted Foreground", defaultValue: "#737373" }),
|
|
23
|
+
accent: color({ label: "Accent", defaultValue: "#f5f5f5" }),
|
|
24
|
+
accentForeground: color({ label: "Accent Foreground", defaultValue: "#171717" }),
|
|
25
|
+
destructive: color({ label: "Destructive", defaultValue: "#ef4444" }),
|
|
26
|
+
destructiveForeground: color({ label: "Destructive Foreground", defaultValue: "#fafafa" }),
|
|
27
|
+
border: color({ label: "Border", defaultValue: "#e5e5e5" }),
|
|
28
|
+
input: color({ label: "Input", defaultValue: "#e5e5e5" }),
|
|
29
|
+
ring: color({ label: "Ring", defaultValue: "#0a0a0a" }),
|
|
30
|
+
chart1: color({ label: "Chart 1", defaultValue: "#e76e50" }),
|
|
31
|
+
chart2: color({ label: "Chart 2", defaultValue: "#2a9d90" }),
|
|
32
|
+
chart3: color({ label: "Chart 3", defaultValue: "#264653" }),
|
|
33
|
+
chart4: color({ label: "Chart 4", defaultValue: "#e8c468" }),
|
|
34
|
+
chart5: color({ label: "Chart 5", defaultValue: "#f4a261" }),
|
|
35
|
+
sidebar: color({ label: "Sidebar", defaultValue: "#fafafa" }),
|
|
36
|
+
sidebarForeground: color({ label: "Sidebar Foreground", defaultValue: "#0a0a0a" }),
|
|
37
|
+
sidebarPrimary: color({ label: "Sidebar Primary", defaultValue: "#171717" }),
|
|
38
|
+
sidebarPrimaryForeground: color({ label: "Sidebar Primary FG", defaultValue: "#fafafa" }),
|
|
39
|
+
sidebarAccent: color({ label: "Sidebar Accent", defaultValue: "#f5f5f5" }),
|
|
40
|
+
sidebarAccentForeground: color({ label: "Sidebar Accent FG", defaultValue: "#171717" }),
|
|
41
|
+
sidebarBorder: color({ label: "Sidebar Border", defaultValue: "#e5e5e5" }),
|
|
42
|
+
sidebarRing: color({ label: "Sidebar Ring", defaultValue: "#0a0a0a" }),
|
|
43
|
+
} as const
|
|
44
|
+
}
|
|
45
|
+
|
|
5
46
|
export const themes = defineCollection({
|
|
6
47
|
slug: TABLE_SLUG_THEMES,
|
|
7
48
|
admin: {
|
|
49
|
+
defaultColumns: ["name", "_id", "fontFamily"],
|
|
8
50
|
group: "Site Builder",
|
|
9
51
|
useAsTitle: "name",
|
|
52
|
+
livePreview: {
|
|
53
|
+
url: "/preview/home",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
versions: {
|
|
57
|
+
drafts: true,
|
|
10
58
|
},
|
|
11
59
|
fields: {
|
|
12
60
|
name: text({
|
|
13
|
-
label: "Name",
|
|
61
|
+
label: "Theme Name",
|
|
14
62
|
required: true,
|
|
15
63
|
}),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
64
|
+
importTheme: ui({
|
|
65
|
+
admin: {
|
|
66
|
+
components: {
|
|
67
|
+
Field: ThemeImportField,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
19
70
|
}),
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
71
|
+
colors: tabs({
|
|
72
|
+
label: "Theme Colors",
|
|
73
|
+
tabs: [
|
|
74
|
+
{
|
|
75
|
+
label: "Light",
|
|
76
|
+
slug: "light",
|
|
77
|
+
fields: themeColorFields(),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: "Dark",
|
|
81
|
+
slug: "dark",
|
|
82
|
+
fields: {
|
|
83
|
+
...themeColorFields(),
|
|
84
|
+
// Override dark mode defaults
|
|
85
|
+
background: color({ label: "Background", defaultValue: "#0a0a0a" }),
|
|
86
|
+
foreground: color({ label: "Foreground", defaultValue: "#fafafa" }),
|
|
87
|
+
card: color({ label: "Card", defaultValue: "#0a0a0a" }),
|
|
88
|
+
cardForeground: color({ label: "Card Foreground", defaultValue: "#fafafa" }),
|
|
89
|
+
popover: color({ label: "Popover", defaultValue: "#0a0a0a" }),
|
|
90
|
+
popoverForeground: color({ label: "Popover Foreground", defaultValue: "#fafafa" }),
|
|
91
|
+
primary: color({ label: "Primary", defaultValue: "#fafafa" }),
|
|
92
|
+
primaryForeground: color({ label: "Primary Foreground", defaultValue: "#171717" }),
|
|
93
|
+
secondary: color({ label: "Secondary", defaultValue: "#262626" }),
|
|
94
|
+
secondaryForeground: color({ label: "Secondary Foreground", defaultValue: "#fafafa" }),
|
|
95
|
+
muted: color({ label: "Muted", defaultValue: "#262626" }),
|
|
96
|
+
mutedForeground: color({ label: "Muted Foreground", defaultValue: "#a3a3a3" }),
|
|
97
|
+
accent: color({ label: "Accent", defaultValue: "#262626" }),
|
|
98
|
+
accentForeground: color({ label: "Accent Foreground", defaultValue: "#fafafa" }),
|
|
99
|
+
border: color({ label: "Border", defaultValue: "#262626" }),
|
|
100
|
+
input: color({ label: "Input", defaultValue: "#262626" }),
|
|
101
|
+
ring: color({ label: "Ring", defaultValue: "#d4d4d4" }),
|
|
102
|
+
sidebar: color({ label: "Sidebar", defaultValue: "#171717" }),
|
|
103
|
+
sidebarForeground: color({ label: "Sidebar Foreground", defaultValue: "#fafafa" }),
|
|
104
|
+
sidebarPrimary: color({ label: "Sidebar Primary", defaultValue: "#fafafa" }),
|
|
105
|
+
sidebarPrimaryForeground: color({
|
|
106
|
+
label: "Sidebar Primary FG",
|
|
107
|
+
defaultValue: "#171717",
|
|
108
|
+
}),
|
|
109
|
+
sidebarAccent: color({ label: "Sidebar Accent", defaultValue: "#262626" }),
|
|
110
|
+
sidebarAccentForeground: color({ label: "Sidebar Accent FG", defaultValue: "#fafafa" }),
|
|
111
|
+
sidebarBorder: color({ label: "Sidebar Border", defaultValue: "#262626" }),
|
|
112
|
+
sidebarRing: color({ label: "Sidebar Ring", defaultValue: "#d4d4d4" }),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
23
116
|
}),
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
117
|
+
radius: text({
|
|
118
|
+
label: "Border Radius",
|
|
119
|
+
defaultValue: "0.625rem",
|
|
27
120
|
}),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
121
|
+
fontFamily: text({
|
|
122
|
+
label: "Font Family",
|
|
123
|
+
defaultValue: "Inter, sans-serif",
|
|
31
124
|
}),
|
|
32
125
|
},
|
|
33
126
|
labels: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { siteSettings } from "./siteSettings"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineGlobal, relationship, text, upload } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import { TABLE_SLUG_MEDIA, TABLE_SLUG_SITE_SETTINGS, TABLE_SLUG_THEMES } from "~/db/constants"
|
|
4
|
+
|
|
5
|
+
export const siteSettings = defineGlobal({
|
|
6
|
+
slug: TABLE_SLUG_SITE_SETTINGS,
|
|
7
|
+
admin: {
|
|
8
|
+
group: "Site Builder",
|
|
9
|
+
livePreview: {
|
|
10
|
+
url: "/preview/home",
|
|
11
|
+
},
|
|
12
|
+
useAsTitle: "name",
|
|
13
|
+
},
|
|
14
|
+
fields: {
|
|
15
|
+
name: text({
|
|
16
|
+
label: "Site Name",
|
|
17
|
+
required: true,
|
|
18
|
+
}),
|
|
19
|
+
activeTheme: relationship({
|
|
20
|
+
label: "Active Theme",
|
|
21
|
+
to: TABLE_SLUG_THEMES,
|
|
22
|
+
}),
|
|
23
|
+
description: text({
|
|
24
|
+
label: "Site Description",
|
|
25
|
+
}),
|
|
26
|
+
favicon: upload({
|
|
27
|
+
admin: { description: "Site favicon image" },
|
|
28
|
+
label: "Favicon",
|
|
29
|
+
to: TABLE_SLUG_MEDIA,
|
|
30
|
+
}),
|
|
31
|
+
googleAnalyticsId: text({
|
|
32
|
+
admin: { description: "GA4 measurement ID (G-XXXXXXXXXX)" },
|
|
33
|
+
label: "Google Analytics ID",
|
|
34
|
+
}),
|
|
35
|
+
metaDescription: text({
|
|
36
|
+
admin: { description: "Default meta description for SEO" },
|
|
37
|
+
label: "Meta Description",
|
|
38
|
+
}),
|
|
39
|
+
metaTitle: text({
|
|
40
|
+
admin: { description: "Default <title> tag for the site" },
|
|
41
|
+
label: "Meta Title",
|
|
42
|
+
}),
|
|
43
|
+
ogImage: upload({
|
|
44
|
+
admin: { description: "Default Open Graph image for social sharing" },
|
|
45
|
+
label: "OG Image",
|
|
46
|
+
to: TABLE_SLUG_MEDIA,
|
|
47
|
+
}),
|
|
48
|
+
twitterHandle: text({
|
|
49
|
+
admin: { description: "@handle for Twitter cards" },
|
|
50
|
+
label: "Twitter Handle",
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
label: "Site Settings",
|
|
54
|
+
versions: {
|
|
55
|
+
drafts: true,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
@@ -2,29 +2,29 @@ import { defineConfig } from "@vexcms/core"
|
|
|
2
2
|
import { convexFileStorage } from "@vexcms/file-storage-convex"
|
|
3
3
|
|
|
4
4
|
import { auth } from "~/vexcms/auth"
|
|
5
|
+
import { access } from "~/vexcms/access"
|
|
5
6
|
import {
|
|
6
7
|
footers,
|
|
7
8
|
headers,
|
|
8
9
|
media,
|
|
9
10
|
pages,
|
|
10
|
-
siteSettings,
|
|
11
11
|
themes,
|
|
12
12
|
users,
|
|
13
13
|
} from "~/vexcms/collections"
|
|
14
|
+
import { siteSettings } from "~/vexcms/globals"
|
|
14
15
|
|
|
15
16
|
export default defineConfig({
|
|
17
|
+
access,
|
|
16
18
|
admin: {
|
|
17
19
|
meta: {
|
|
18
20
|
titleSuffix: " | My Site",
|
|
19
21
|
},
|
|
20
|
-
sidebar: {
|
|
21
|
-
hideGlobals: true,
|
|
22
|
-
},
|
|
23
22
|
user: "user",
|
|
24
23
|
},
|
|
25
24
|
auth,
|
|
26
25
|
basePath: "/admin",
|
|
27
|
-
collections: [pages, headers, footers, themes,
|
|
26
|
+
collections: [pages, headers, footers, themes, users],
|
|
27
|
+
globals: [siteSettings],
|
|
28
28
|
media: {
|
|
29
29
|
collections: [media],
|
|
30
30
|
storageAdapter: convexFileStorage(),
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import type { User } from "~/db/types"
|
|
2
|
-
|
|
3
|
-
import { TABLE_SLUG_USERS, USER_ROLES, type UserRole } from "~/db/constants"
|
|
4
|
-
|
|
5
|
-
export const AUTH_ACTIONS = {
|
|
6
|
-
create: "create",
|
|
7
|
-
delete: "delete",
|
|
8
|
-
read: "read",
|
|
9
|
-
update: "update",
|
|
10
|
-
} as const
|
|
11
|
-
export type AuthAction = (typeof AUTH_ACTIONS)[keyof typeof AUTH_ACTIONS]
|
|
12
|
-
|
|
13
|
-
export const RESOURCES = {
|
|
14
|
-
users: TABLE_SLUG_USERS,
|
|
15
|
-
} as const
|
|
16
|
-
export type Permissions = {
|
|
17
|
-
[TABLE_SLUG_USERS]: {
|
|
18
|
-
action: AuthAction
|
|
19
|
-
dataType: User
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type Resource = (typeof RESOURCES)[keyof typeof RESOURCES]
|
|
24
|
-
|
|
25
|
-
export type RolesWithPermissions = Record<
|
|
26
|
-
UserRole,
|
|
27
|
-
Partial<{
|
|
28
|
-
[Key in keyof Permissions]: Partial<Record<Permissions[Key]["action"], PermissionCheck<Key>>>
|
|
29
|
-
}>
|
|
30
|
-
>
|
|
31
|
-
|
|
32
|
-
type PermissionCheck<Key extends keyof Permissions> =
|
|
33
|
-
| (({ data, user }: { data: Permissions[Key]["dataType"]; user: User }) => boolean)
|
|
34
|
-
| boolean
|
|
35
|
-
|
|
36
|
-
// type PermissionDataTypeExcluding<K extends keyof Permissions> = Permissions[Exclude<keyof Permissions, K>]["dataType"];
|
|
37
|
-
// usage: T extends PermissionDataTypeExcluding<"orgs" | "users">
|
|
38
|
-
|
|
39
|
-
const ROLES = {
|
|
40
|
-
[USER_ROLES.admin]: {
|
|
41
|
-
[TABLE_SLUG_USERS]: {
|
|
42
|
-
create: true,
|
|
43
|
-
delete: true,
|
|
44
|
-
read: true,
|
|
45
|
-
update: true,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
[USER_ROLES.editor]: {
|
|
49
|
-
[TABLE_SLUG_USERS]: {
|
|
50
|
-
read: true,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
[USER_ROLES.author]: {
|
|
54
|
-
[TABLE_SLUG_USERS]: {
|
|
55
|
-
read: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
56
|
-
update: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
[USER_ROLES.member]: {
|
|
60
|
-
[TABLE_SLUG_USERS]: {
|
|
61
|
-
read: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
[USER_ROLES.user]: {
|
|
65
|
-
[TABLE_SLUG_USERS]: {
|
|
66
|
-
create: false,
|
|
67
|
-
delete: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
68
|
-
read: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
69
|
-
update: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
} as const satisfies RolesWithPermissions
|
|
73
|
-
|
|
74
|
-
export function hasPermission<Resource extends keyof Permissions>({
|
|
75
|
-
action,
|
|
76
|
-
data,
|
|
77
|
-
resource,
|
|
78
|
-
user,
|
|
79
|
-
}: {
|
|
80
|
-
action: Permissions[Resource]["action"]
|
|
81
|
-
data?: Permissions[Resource]["dataType"]
|
|
82
|
-
resource: Resource
|
|
83
|
-
user?: null | User
|
|
84
|
-
}): boolean {
|
|
85
|
-
if (!user?.role) {
|
|
86
|
-
return false
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return user.role.some((role) => {
|
|
90
|
-
const permission = (ROLES as RolesWithPermissions)[role as UserRole][resource]?.[action]
|
|
91
|
-
|
|
92
|
-
if (!permission) {
|
|
93
|
-
return false
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (typeof permission === "boolean") {
|
|
97
|
-
return permission
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return data != null && permission({ data, user })
|
|
101
|
-
})
|
|
102
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { defineCollection, text, upload } from "@vexcms/core"
|
|
2
|
-
|
|
3
|
-
import { TABLE_SLUG_MEDIA, TABLE_SLUG_SITE_SETTINGS } from "~/db/constants"
|
|
4
|
-
|
|
5
|
-
export const siteSettings = defineCollection({
|
|
6
|
-
slug: TABLE_SLUG_SITE_SETTINGS,
|
|
7
|
-
admin: {
|
|
8
|
-
group: "Site Builder",
|
|
9
|
-
useAsTitle: "name",
|
|
10
|
-
},
|
|
11
|
-
fields: {
|
|
12
|
-
name: text({
|
|
13
|
-
label: "Site Name",
|
|
14
|
-
required: true,
|
|
15
|
-
}),
|
|
16
|
-
description: text({
|
|
17
|
-
label: "Site Description",
|
|
18
|
-
}),
|
|
19
|
-
favicon: upload({
|
|
20
|
-
admin: {
|
|
21
|
-
description: "Site favicon image",
|
|
22
|
-
},
|
|
23
|
-
label: "Favicon",
|
|
24
|
-
to: TABLE_SLUG_MEDIA,
|
|
25
|
-
}),
|
|
26
|
-
},
|
|
27
|
-
labels: {
|
|
28
|
-
plural: "Site Settings",
|
|
29
|
-
singular: "Site Settings",
|
|
30
|
-
},
|
|
31
|
-
})
|