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,138 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { motion, type Variants } from "motion/react"
|
|
4
|
+
import React, { type ComponentPropsWithRef } from "react"
|
|
5
|
+
|
|
6
|
+
export type PresetType =
|
|
7
|
+
| "blur"
|
|
8
|
+
| "blur-slide"
|
|
9
|
+
| "bounce"
|
|
10
|
+
| "fade"
|
|
11
|
+
| "flip"
|
|
12
|
+
| "rotate"
|
|
13
|
+
| "scale"
|
|
14
|
+
| "slide"
|
|
15
|
+
| "swing"
|
|
16
|
+
| "zoom"
|
|
17
|
+
|
|
18
|
+
export type AnimatedGroupProps = ComponentPropsWithRef<"div"> & {
|
|
19
|
+
as?: React.ElementType
|
|
20
|
+
asChild?: React.ElementType
|
|
21
|
+
className?: string
|
|
22
|
+
preset?: PresetType
|
|
23
|
+
variants?: {
|
|
24
|
+
container?: Variants
|
|
25
|
+
item?: Variants
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultContainerVariants: Variants = {
|
|
30
|
+
visible: {
|
|
31
|
+
transition: {
|
|
32
|
+
staggerChildren: 0.1,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const defaultItemVariants: Variants = {
|
|
38
|
+
hidden: { opacity: 0 },
|
|
39
|
+
visible: { opacity: 1 },
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const presetVariants: Record<PresetType, Variants> = {
|
|
43
|
+
"blur-slide": {
|
|
44
|
+
hidden: { filter: "blur(4px)", y: 20 },
|
|
45
|
+
visible: { filter: "blur(0px)", y: 0 },
|
|
46
|
+
},
|
|
47
|
+
slide: {
|
|
48
|
+
hidden: { y: 20 },
|
|
49
|
+
visible: { y: 0 },
|
|
50
|
+
},
|
|
51
|
+
blur: {
|
|
52
|
+
hidden: { filter: "blur(4px)" },
|
|
53
|
+
visible: { filter: "blur(0px)" },
|
|
54
|
+
},
|
|
55
|
+
bounce: {
|
|
56
|
+
hidden: { y: -50 },
|
|
57
|
+
visible: {
|
|
58
|
+
transition: { type: "spring", damping: 10, stiffness: 400 },
|
|
59
|
+
y: 0,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
fade: {},
|
|
63
|
+
flip: {
|
|
64
|
+
hidden: { rotateX: -90 },
|
|
65
|
+
visible: {
|
|
66
|
+
rotateX: 0,
|
|
67
|
+
transition: { type: "spring", damping: 20, stiffness: 300 },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
rotate: {
|
|
71
|
+
hidden: { rotate: -180 },
|
|
72
|
+
visible: {
|
|
73
|
+
rotate: 0,
|
|
74
|
+
transition: { type: "spring", damping: 15, stiffness: 200 },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
scale: {
|
|
78
|
+
hidden: { scale: 0.8 },
|
|
79
|
+
visible: { scale: 1 },
|
|
80
|
+
},
|
|
81
|
+
swing: {
|
|
82
|
+
hidden: { rotate: -10 },
|
|
83
|
+
visible: {
|
|
84
|
+
rotate: 0,
|
|
85
|
+
transition: { type: "spring", damping: 8, stiffness: 300 },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
zoom: {
|
|
89
|
+
hidden: { scale: 0.5 },
|
|
90
|
+
visible: {
|
|
91
|
+
scale: 1,
|
|
92
|
+
transition: { type: "spring", damping: 20, stiffness: 300 },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const addDefaultVariants = (variants: Variants) => ({
|
|
98
|
+
hidden: { ...defaultItemVariants.hidden, ...variants.hidden },
|
|
99
|
+
visible: { ...defaultItemVariants.visible, ...variants.visible },
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
function AnimatedGroup({
|
|
103
|
+
as = "div",
|
|
104
|
+
asChild = "div",
|
|
105
|
+
children,
|
|
106
|
+
className,
|
|
107
|
+
preset,
|
|
108
|
+
variants,
|
|
109
|
+
...divProps
|
|
110
|
+
}: AnimatedGroupProps) {
|
|
111
|
+
const selectedVariants = {
|
|
112
|
+
container: addDefaultVariants(defaultContainerVariants),
|
|
113
|
+
item: addDefaultVariants(preset ? presetVariants[preset] : {}),
|
|
114
|
+
}
|
|
115
|
+
const containerVariants = variants?.container ?? selectedVariants.container
|
|
116
|
+
const itemVariants = variants?.item ?? selectedVariants.item
|
|
117
|
+
|
|
118
|
+
const MotionComponent = motion(as)
|
|
119
|
+
const MotionChild = motion(asChild)
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<MotionComponent
|
|
123
|
+
animate="visible"
|
|
124
|
+
className={className}
|
|
125
|
+
initial="hidden"
|
|
126
|
+
variants={containerVariants}
|
|
127
|
+
{...divProps}
|
|
128
|
+
>
|
|
129
|
+
{React.Children.map(children, (child, index) => (
|
|
130
|
+
<MotionChild key={index} variants={itemVariants}>
|
|
131
|
+
{child}
|
|
132
|
+
</MotionChild>
|
|
133
|
+
))}
|
|
134
|
+
</MotionComponent>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { AnimatedGroup }
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AnimatePresence,
|
|
5
|
+
motion,
|
|
6
|
+
type TargetAndTransition,
|
|
7
|
+
type Transition,
|
|
8
|
+
type Variant,
|
|
9
|
+
type Variants,
|
|
10
|
+
} from "motion/react"
|
|
11
|
+
import React from "react"
|
|
12
|
+
|
|
13
|
+
import { cn } from "~/lib/utils"
|
|
14
|
+
|
|
15
|
+
export type PerType = "char" | "line" | "word"
|
|
16
|
+
export type PresetType = "blur" | "fade" | "fade-in-blur" | "scale" | "slide"
|
|
17
|
+
|
|
18
|
+
export type TextEffectProps = {
|
|
19
|
+
as?: keyof React.JSX.IntrinsicElements
|
|
20
|
+
children: string
|
|
21
|
+
className?: string
|
|
22
|
+
containerTransition?: Transition
|
|
23
|
+
delay?: number
|
|
24
|
+
onAnimationComplete?: () => void
|
|
25
|
+
onAnimationStart?: () => void
|
|
26
|
+
per?: PerType
|
|
27
|
+
preset?: PresetType
|
|
28
|
+
segmentTransition?: Transition
|
|
29
|
+
segmentWrapperClassName?: string
|
|
30
|
+
speedReveal?: number
|
|
31
|
+
speedSegment?: number
|
|
32
|
+
style?: React.CSSProperties
|
|
33
|
+
trigger?: boolean
|
|
34
|
+
variants?: {
|
|
35
|
+
container?: Variants
|
|
36
|
+
item?: Variants
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaultStaggerTimes: Record<PerType, number> = {
|
|
41
|
+
char: 0.03,
|
|
42
|
+
line: 0.1,
|
|
43
|
+
word: 0.05,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const defaultContainerVariants: Variants = {
|
|
47
|
+
hidden: { opacity: 0 },
|
|
48
|
+
exit: {
|
|
49
|
+
transition: { staggerChildren: 0.05, staggerDirection: -1 },
|
|
50
|
+
},
|
|
51
|
+
visible: {
|
|
52
|
+
opacity: 1,
|
|
53
|
+
transition: {
|
|
54
|
+
staggerChildren: 0.05,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultItemVariants: Variants = {
|
|
60
|
+
hidden: { opacity: 0 },
|
|
61
|
+
exit: { opacity: 0 },
|
|
62
|
+
visible: {
|
|
63
|
+
opacity: 1,
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const presetVariants: Record<
|
|
68
|
+
PresetType,
|
|
69
|
+
{ container: Variants; item: Variants }
|
|
70
|
+
> = {
|
|
71
|
+
slide: {
|
|
72
|
+
container: defaultContainerVariants,
|
|
73
|
+
item: {
|
|
74
|
+
hidden: { opacity: 0, y: 20 },
|
|
75
|
+
exit: { opacity: 0, y: 20 },
|
|
76
|
+
visible: { opacity: 1, y: 0 },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
blur: {
|
|
80
|
+
container: defaultContainerVariants,
|
|
81
|
+
item: {
|
|
82
|
+
hidden: { filter: "blur(12px)", opacity: 0 },
|
|
83
|
+
exit: { filter: "blur(12px)", opacity: 0 },
|
|
84
|
+
visible: { filter: "blur(0px)", opacity: 1 },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
fade: {
|
|
88
|
+
container: defaultContainerVariants,
|
|
89
|
+
item: {
|
|
90
|
+
hidden: { opacity: 0 },
|
|
91
|
+
exit: { opacity: 0 },
|
|
92
|
+
visible: { opacity: 1 },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
"fade-in-blur": {
|
|
96
|
+
container: defaultContainerVariants,
|
|
97
|
+
item: {
|
|
98
|
+
hidden: { filter: "blur(12px)", opacity: 0, y: 20 },
|
|
99
|
+
exit: { filter: "blur(12px)", opacity: 0, y: 20 },
|
|
100
|
+
visible: { filter: "blur(0px)", opacity: 1, y: 0 },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
scale: {
|
|
104
|
+
container: defaultContainerVariants,
|
|
105
|
+
item: {
|
|
106
|
+
hidden: { opacity: 0, scale: 0 },
|
|
107
|
+
exit: { opacity: 0, scale: 0 },
|
|
108
|
+
visible: { opacity: 1, scale: 1 },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const AnimationComponent: React.FC<{
|
|
114
|
+
per: "char" | "line" | "word"
|
|
115
|
+
segment: string
|
|
116
|
+
segmentWrapperClassName?: string
|
|
117
|
+
variants: Variants
|
|
118
|
+
}> = React.memo(({ per, segment, segmentWrapperClassName, variants }) => {
|
|
119
|
+
const content =
|
|
120
|
+
per === "line" ? (
|
|
121
|
+
<motion.span className="block" variants={variants}>
|
|
122
|
+
{segment}
|
|
123
|
+
</motion.span>
|
|
124
|
+
) : per === "word" ? (
|
|
125
|
+
<motion.span
|
|
126
|
+
aria-hidden="true"
|
|
127
|
+
className="inline-block whitespace-pre"
|
|
128
|
+
variants={variants}
|
|
129
|
+
>
|
|
130
|
+
{segment}
|
|
131
|
+
</motion.span>
|
|
132
|
+
) : (
|
|
133
|
+
<motion.span className="inline-block whitespace-pre">
|
|
134
|
+
{segment.split("").map((char, charIndex) => (
|
|
135
|
+
<motion.span
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
className="inline-block whitespace-pre"
|
|
138
|
+
key={`char-${charIndex}`}
|
|
139
|
+
variants={variants}
|
|
140
|
+
>
|
|
141
|
+
{char}
|
|
142
|
+
</motion.span>
|
|
143
|
+
))}
|
|
144
|
+
</motion.span>
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (!segmentWrapperClassName) {
|
|
148
|
+
return content
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const defaultWrapperClassName = per === "line" ? "block" : "inline-block"
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>
|
|
155
|
+
{content}
|
|
156
|
+
</span>
|
|
157
|
+
)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
AnimationComponent.displayName = "AnimationComponent"
|
|
161
|
+
|
|
162
|
+
const splitText = (text: string, per: "char" | "line" | "word") => {
|
|
163
|
+
if (per === "line") return text.split("\n")
|
|
164
|
+
return text.split(/(\s+)/)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const hasTransition = (
|
|
168
|
+
variant: Variant,
|
|
169
|
+
): variant is TargetAndTransition & { transition?: Transition } => {
|
|
170
|
+
return (
|
|
171
|
+
typeof variant === "object" && variant !== null && "transition" in variant
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const createVariantsWithTransition = (
|
|
176
|
+
baseVariants: Variants,
|
|
177
|
+
transition?: Transition & { exit?: Transition },
|
|
178
|
+
): Variants => {
|
|
179
|
+
if (!transition) return baseVariants
|
|
180
|
+
|
|
181
|
+
const { ...mainTransition } = transition
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...baseVariants,
|
|
185
|
+
exit: {
|
|
186
|
+
...baseVariants.exit,
|
|
187
|
+
transition: {
|
|
188
|
+
...(hasTransition(baseVariants.exit)
|
|
189
|
+
? baseVariants.exit.transition
|
|
190
|
+
: {}),
|
|
191
|
+
...mainTransition,
|
|
192
|
+
staggerDirection: -1,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
visible: {
|
|
196
|
+
...baseVariants.visible,
|
|
197
|
+
transition: {
|
|
198
|
+
...(hasTransition(baseVariants.visible)
|
|
199
|
+
? baseVariants.visible.transition
|
|
200
|
+
: {}),
|
|
201
|
+
...mainTransition,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function TextEffect({
|
|
208
|
+
as = "p",
|
|
209
|
+
children,
|
|
210
|
+
className,
|
|
211
|
+
containerTransition,
|
|
212
|
+
delay = 0,
|
|
213
|
+
onAnimationComplete,
|
|
214
|
+
onAnimationStart,
|
|
215
|
+
per = "word",
|
|
216
|
+
preset = "fade",
|
|
217
|
+
segmentTransition,
|
|
218
|
+
segmentWrapperClassName,
|
|
219
|
+
speedReveal = 1,
|
|
220
|
+
speedSegment = 1,
|
|
221
|
+
style,
|
|
222
|
+
trigger = true,
|
|
223
|
+
variants,
|
|
224
|
+
}: TextEffectProps) {
|
|
225
|
+
const segments = splitText(children, per)
|
|
226
|
+
const MotionTag = motion[as as keyof typeof motion] as typeof motion.div
|
|
227
|
+
|
|
228
|
+
const baseVariants = preset
|
|
229
|
+
? presetVariants[preset]
|
|
230
|
+
: { container: defaultContainerVariants, item: defaultItemVariants }
|
|
231
|
+
|
|
232
|
+
const stagger = defaultStaggerTimes[per] / speedReveal
|
|
233
|
+
|
|
234
|
+
const baseDuration = 0.3 / speedSegment
|
|
235
|
+
|
|
236
|
+
const customStagger = hasTransition(variants?.container?.visible ?? {})
|
|
237
|
+
? (variants?.container?.visible as TargetAndTransition).transition
|
|
238
|
+
?.staggerChildren
|
|
239
|
+
: undefined
|
|
240
|
+
|
|
241
|
+
const customDelay = hasTransition(variants?.container?.visible ?? {})
|
|
242
|
+
? (variants?.container?.visible as TargetAndTransition).transition
|
|
243
|
+
?.delayChildren
|
|
244
|
+
: undefined
|
|
245
|
+
|
|
246
|
+
const computedVariants = {
|
|
247
|
+
container: createVariantsWithTransition(
|
|
248
|
+
variants?.container ?? baseVariants.container,
|
|
249
|
+
{
|
|
250
|
+
delayChildren: customDelay ?? delay,
|
|
251
|
+
staggerChildren: customStagger ?? stagger,
|
|
252
|
+
...containerTransition,
|
|
253
|
+
exit: {
|
|
254
|
+
staggerChildren: customStagger ?? stagger,
|
|
255
|
+
staggerDirection: -1,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
),
|
|
259
|
+
item: createVariantsWithTransition(variants?.item ?? baseVariants.item, {
|
|
260
|
+
duration: baseDuration,
|
|
261
|
+
...segmentTransition,
|
|
262
|
+
}),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<AnimatePresence mode="popLayout">
|
|
267
|
+
{trigger && (
|
|
268
|
+
<MotionTag
|
|
269
|
+
animate="visible"
|
|
270
|
+
className={className}
|
|
271
|
+
exit="exit"
|
|
272
|
+
initial="hidden"
|
|
273
|
+
onAnimationComplete={onAnimationComplete}
|
|
274
|
+
onAnimationStart={onAnimationStart}
|
|
275
|
+
style={style}
|
|
276
|
+
variants={computedVariants.container}
|
|
277
|
+
>
|
|
278
|
+
{per !== "line" ? <span className="sr-only">{children}</span> : null}
|
|
279
|
+
{segments.map((segment, index) => (
|
|
280
|
+
<AnimationComponent
|
|
281
|
+
key={`${per}-${index}-${segment}`}
|
|
282
|
+
per={per}
|
|
283
|
+
segment={segment}
|
|
284
|
+
segmentWrapperClassName={segmentWrapperClassName}
|
|
285
|
+
variants={computedVariants.item}
|
|
286
|
+
/>
|
|
287
|
+
))}
|
|
288
|
+
</MotionTag>
|
|
289
|
+
)}
|
|
290
|
+
</AnimatePresence>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
|
|
3
|
+
import { buildSiteMetadata } from "@vexcms/core"
|
|
4
|
+
import { fetchQuery } from "convex/nextjs"
|
|
5
|
+
|
|
6
|
+
import { api } from "@convex/_generated/api"
|
|
7
|
+
import { anyApi } from "convex/server"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate Next.js Metadata for a page.
|
|
11
|
+
* Fetches site settings and optionally a page document,
|
|
12
|
+
* merges with buildSiteMetadata, and returns Next.js Metadata format.
|
|
13
|
+
*
|
|
14
|
+
* @param props.slug - Optional page slug to fetch per-page SEO overrides
|
|
15
|
+
*/
|
|
16
|
+
export async function generatePageMetadata(props: {
|
|
17
|
+
slug?: string
|
|
18
|
+
}): Promise<Metadata> {
|
|
19
|
+
try {
|
|
20
|
+
// Fetch site settings
|
|
21
|
+
const settings = (await fetchQuery(api.siteSettings.get)) as Record<
|
|
22
|
+
string,
|
|
23
|
+
unknown
|
|
24
|
+
> | null
|
|
25
|
+
|
|
26
|
+
if (!settings) {
|
|
27
|
+
return { title: "Untitled" }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Resolve the ogImage URL from site settings (upload ID → URL)
|
|
31
|
+
let siteOgImageUrl: string | undefined
|
|
32
|
+
const ogImageId = settings.ogImage as string | undefined
|
|
33
|
+
if (ogImageId) {
|
|
34
|
+
try {
|
|
35
|
+
const mediaDoc = (await fetchQuery(
|
|
36
|
+
anyApi.vex.collections.getDocument,
|
|
37
|
+
{
|
|
38
|
+
collectionSlug: "media",
|
|
39
|
+
documentId: ogImageId,
|
|
40
|
+
}
|
|
41
|
+
)) as Record<string, unknown> | null
|
|
42
|
+
siteOgImageUrl = (mediaDoc?.url as string) || undefined
|
|
43
|
+
} catch {
|
|
44
|
+
// Media document not found — skip ogImage
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fetch page data for per-page overrides
|
|
49
|
+
let pageData: Record<string, unknown> | null = null
|
|
50
|
+
if (props.slug) {
|
|
51
|
+
pageData = await fetchQuery(api.pages.getBySlug, {
|
|
52
|
+
slug: props.slug,
|
|
53
|
+
_vexDrafts: false,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build framework-agnostic metadata
|
|
58
|
+
const meta = buildSiteMetadata({
|
|
59
|
+
site: {
|
|
60
|
+
name: settings.name as string | undefined,
|
|
61
|
+
metaTitle: settings.metaTitle as string | undefined,
|
|
62
|
+
metaDescription: settings.metaDescription as string | undefined,
|
|
63
|
+
description: settings.description as string | undefined,
|
|
64
|
+
ogImage: siteOgImageUrl,
|
|
65
|
+
twitterHandle: settings.twitterHandle as string | undefined,
|
|
66
|
+
},
|
|
67
|
+
page: pageData
|
|
68
|
+
? {
|
|
69
|
+
title: pageData.title as string | undefined,
|
|
70
|
+
metaTitle: pageData.metaTitle as string | undefined,
|
|
71
|
+
metaDescription: pageData.metaDescription as string | undefined,
|
|
72
|
+
ogImage: pageData.ogImage as string | undefined,
|
|
73
|
+
}
|
|
74
|
+
: undefined,
|
|
75
|
+
titleSuffix: " | Vex CMS",
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Map to Next.js Metadata
|
|
79
|
+
const metadata: Metadata = {
|
|
80
|
+
title: meta.title,
|
|
81
|
+
description: meta.description,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (meta.ogImage) {
|
|
85
|
+
metadata.openGraph = {
|
|
86
|
+
title: meta.title,
|
|
87
|
+
description: meta.description,
|
|
88
|
+
images: [{ url: meta.ogImage }],
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (meta.twitterHandle) {
|
|
93
|
+
metadata.twitter = {
|
|
94
|
+
card: "summary_large_image",
|
|
95
|
+
site: meta.twitterHandle,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return metadata
|
|
100
|
+
} catch {
|
|
101
|
+
// Convex not available — return minimal metadata
|
|
102
|
+
return { title: "Vex CMS" }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineAccess } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import { TABLE_SLUG_USERS, USER_ROLES } from "~/db/constants"
|
|
4
|
+
import { users } from "~/vexcms/collections"
|
|
5
|
+
|
|
6
|
+
export const access = defineAccess({
|
|
7
|
+
permissions: {
|
|
8
|
+
admin: {
|
|
9
|
+
admin: true,
|
|
10
|
+
user: true,
|
|
11
|
+
},
|
|
12
|
+
user: {
|
|
13
|
+
[TABLE_SLUG_USERS]: {
|
|
14
|
+
create: false,
|
|
15
|
+
delete: false,
|
|
16
|
+
read: ({ data: targetUser, user }) => {
|
|
17
|
+
return targetUser._id === user._id
|
|
18
|
+
},
|
|
19
|
+
update: ({ data: targetUser, user }) => {
|
|
20
|
+
return targetUser._id === user._id
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
resources: [users],
|
|
26
|
+
roles: [USER_ROLES.user, USER_ROLES.admin],
|
|
27
|
+
userCollection: users,
|
|
28
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { array, defineBlock, object, text } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import { BLOCK_SLUG_CTA } from "../constants"
|
|
4
|
+
|
|
5
|
+
export const ctaBlock = defineBlock({
|
|
6
|
+
slug: BLOCK_SLUG_CTA,
|
|
7
|
+
label: "Call to Action",
|
|
8
|
+
fields: {
|
|
9
|
+
heading: text({
|
|
10
|
+
label: "Heading",
|
|
11
|
+
required: true,
|
|
12
|
+
defaultValue: "Ready to build with Vex CMS?",
|
|
13
|
+
}),
|
|
14
|
+
subheading: text({
|
|
15
|
+
label: "Subheading",
|
|
16
|
+
defaultValue:
|
|
17
|
+
"Get started in minutes with create-vexcms. Real-time content management powered by Convex.",
|
|
18
|
+
}),
|
|
19
|
+
actions: array({
|
|
20
|
+
label: "Actions",
|
|
21
|
+
items: object({
|
|
22
|
+
fields: {
|
|
23
|
+
label: text({ label: "Button Label", required: true }),
|
|
24
|
+
href: text({ label: "Button Link", required: true }),
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
defaultValue: [
|
|
28
|
+
{ label: "Get Started", href: "/docs" },
|
|
29
|
+
{ label: "View on GitHub", href: "https://github.com/vexcms/vex" },
|
|
30
|
+
],
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
admin: {
|
|
34
|
+
icon: "megaphone",
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BlockComponentProps } from "@vexcms/ui"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
|
|
5
|
+
import { cn } from "~/lib/utils"
|
|
6
|
+
import { buttonVariants } from "~/components/ui/button"
|
|
7
|
+
|
|
8
|
+
export { ctaBlock } from "./config"
|
|
9
|
+
|
|
10
|
+
export default function CTABlock({ block }: BlockComponentProps) {
|
|
11
|
+
const { heading, subheading, actions } = block as unknown as {
|
|
12
|
+
heading: string
|
|
13
|
+
subheading?: string
|
|
14
|
+
actions?: Array<{ label: string; href: string }>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<section className="py-16 md:py-32">
|
|
19
|
+
<div className="mx-auto max-w-5xl px-6">
|
|
20
|
+
<div className="text-center">
|
|
21
|
+
<h2 className="text-4xl font-semibold text-balance lg:text-5xl">
|
|
22
|
+
{heading}
|
|
23
|
+
</h2>
|
|
24
|
+
{subheading && <p className="mt-4">{subheading}</p>}
|
|
25
|
+
|
|
26
|
+
<div className="mt-12 flex flex-wrap justify-center gap-4">
|
|
27
|
+
{(actions ?? []).map((action, index) => (
|
|
28
|
+
<Link
|
|
29
|
+
key={index}
|
|
30
|
+
href={action.href ?? "/"}
|
|
31
|
+
className={cn(
|
|
32
|
+
buttonVariants({
|
|
33
|
+
size: "lg",
|
|
34
|
+
variant: index % 2 === 1 ? "outline" : "default",
|
|
35
|
+
})
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
<span>{action.label}</span>
|
|
39
|
+
</Link>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|
|
45
|
+
)
|
|
46
|
+
}
|