create-vexcms 0.0.20 → 0.1.0-alpha.3
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 +98 -80
- package/dist/index.js +974 -639
- package/dist/index.mjs +30976 -0
- package/package.json +35 -15
- package/templates/base-nextjs/README.md +59 -203
- 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/tsconfig.json +2 -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 +46 -48
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
- package/templates/base-nextjs/src/app/globals.css +240 -104
- package/templates/base-nextjs/src/app/layout.tsx +23 -13
- package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
- package/templates/base-nextjs/src/auth/access.ts +46 -0
- package/templates/base-nextjs/src/auth/client.tsx +17 -5
- package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
- package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
- package/templates/base-nextjs/src/components/component-example.tsx +56 -464
- package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
- package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
- package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
- package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
- package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
- package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
- package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
- package/templates/base-nextjs/src/db/constants/index.ts +22 -35
- package/templates/base-nextjs/src/lib/utils.ts +0 -12
- package/templates/base-nextjs/src/proxy.ts +97 -12
- package/templates/base-nextjs/src/vex.config.ts +35 -0
- package/templates/base-nextjs/src/vex.types.ts +293 -0
- package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
- package/templates/base-nextjs/tsconfig.json +3 -0
- package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
- package/templates/marketing-site/convex/_generated/api.js +23 -0
- package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
- package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
- package/templates/marketing-site/convex/_generated/server.js +101 -0
- package/templates/marketing-site/convex/footers.ts +11 -7
- package/templates/marketing-site/convex/headers.ts +15 -5
- package/templates/marketing-site/convex/pages.ts +20 -35
- package/templates/marketing-site/convex/schema.ts +119 -0
- package/templates/marketing-site/convex/seed.ts +582 -35
- package/templates/marketing-site/convex/siteSettings.ts +19 -5
- package/templates/marketing-site/convex/theme.ts +55 -61
- package/templates/marketing-site/convex/vex.schema.ts +355 -0
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
- package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
- package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
- package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
- package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
- package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
- package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
- package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
- package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +41 -267
- 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 +12 -55
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
- package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
- package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
- package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
- package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
- package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
- package/templates/base-nextjs/convex/auth/config.ts +0 -10
- package/templates/base-nextjs/convex/vex/auth.ts +0 -38
- package/templates/base-nextjs/convex/vex/collections.ts +0 -333
- package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
- package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
- package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
- package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
- package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
- package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
- package/templates/base-nextjs/convex/vex/versions.ts +0 -648
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
- package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
- package/templates/base-nextjs/src/db/types.ts +0 -6
- package/templates/base-nextjs/src/vexcms/access.ts +0 -44
- package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
- package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
- package/templates/base-nextjs/vex.config.ts +0 -25
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
- package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
- package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
- package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
- package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
- package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
- package/templates/marketing-site/src/db/constants/index.ts +0 -52
- package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
- package/templates/marketing-site/src/vexcms/access.ts +0 -36
- package/templates/marketing-site/vex.config.ts +0 -32
|
@@ -1,138 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
import React, { type ComponentPropsWithRef, type CSSProperties } from "react"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import React, { type ComponentPropsWithRef } from "react"
|
|
3
|
+
import { cn } from "~/lib/utils"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
| "blur"
|
|
8
|
-
| "blur-slide"
|
|
9
|
-
| "bounce"
|
|
10
|
-
| "fade"
|
|
11
|
-
| "flip"
|
|
12
|
-
| "rotate"
|
|
13
|
-
| "scale"
|
|
14
|
-
| "slide"
|
|
15
|
-
| "swing"
|
|
16
|
-
| "zoom"
|
|
5
|
+
import styles from "./reveal.module.css"
|
|
17
6
|
|
|
18
7
|
export type AnimatedGroupProps = ComponentPropsWithRef<"div"> & {
|
|
19
8
|
as?: React.ElementType
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
container?: Variants
|
|
25
|
-
item?: Variants
|
|
26
|
-
}
|
|
9
|
+
/** Seconds before the first child starts animating. */
|
|
10
|
+
delay?: number
|
|
11
|
+
/** Seconds between each child's start. */
|
|
12
|
+
stagger?: number
|
|
27
13
|
}
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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",
|
|
15
|
+
/**
|
|
16
|
+
* Staggers a blur + fade + rise entrance across its direct children using a
|
|
17
|
+
* pure-CSS keyframe (see `reveal.module.css`) — no animation-library
|
|
18
|
+
* dependency. Each child is wrapped in a div carrying the animation with an
|
|
19
|
+
* incremented `animation-delay`.
|
|
20
|
+
*/
|
|
21
|
+
export function AnimatedGroup({
|
|
22
|
+
as: Tag = "div",
|
|
105
23
|
children,
|
|
106
24
|
className,
|
|
107
|
-
|
|
108
|
-
|
|
25
|
+
delay = 0,
|
|
26
|
+
stagger = 0.05,
|
|
109
27
|
...divProps
|
|
110
28
|
}: 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.create(as)
|
|
119
|
-
const MotionChild = motion.create(asChild)
|
|
120
|
-
|
|
121
29
|
return (
|
|
122
|
-
<
|
|
123
|
-
animate="visible"
|
|
124
|
-
className={className}
|
|
125
|
-
initial="hidden"
|
|
126
|
-
variants={containerVariants}
|
|
127
|
-
{...divProps}
|
|
128
|
-
>
|
|
30
|
+
<Tag className={className} {...divProps}>
|
|
129
31
|
{React.Children.map(children, (child, index) => (
|
|
130
|
-
<
|
|
32
|
+
<div
|
|
33
|
+
className={styles.item}
|
|
34
|
+
style={{ "--reveal-delay": `${delay + index * stagger}s` } as CSSProperties}
|
|
35
|
+
>
|
|
131
36
|
{child}
|
|
132
|
-
</
|
|
37
|
+
</div>
|
|
133
38
|
))}
|
|
134
|
-
</
|
|
39
|
+
</Tag>
|
|
135
40
|
)
|
|
136
41
|
}
|
|
137
42
|
|
|
138
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Applies the same reveal animation to a single element without adding a
|
|
45
|
+
* wrapper div — for callers that only need one animated child.
|
|
46
|
+
*/
|
|
47
|
+
export function AnimatedItem({
|
|
48
|
+
children,
|
|
49
|
+
className,
|
|
50
|
+
delay = 0,
|
|
51
|
+
...divProps
|
|
52
|
+
}: ComponentPropsWithRef<"div"> & { delay?: number }) {
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={cn(styles.item, className)}
|
|
56
|
+
style={{ "--reveal-delay": `${delay}s` } as CSSProperties}
|
|
57
|
+
{...divProps}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared "reveal" animation for AnimatedGroup and TextEffect — the
|
|
3
|
+
* blur + fade + rise entrance the hero uses, implemented in pure CSS so the
|
|
4
|
+
* template carries no animation-library dependency.
|
|
5
|
+
*
|
|
6
|
+
* `both` fill-mode keeps elements hidden until their delay elapses, which is
|
|
7
|
+
* what makes the stagger read as a sequence. Delay per element arrives via
|
|
8
|
+
* the `--reveal-delay` custom property.
|
|
9
|
+
*/
|
|
10
|
+
@keyframes reveal {
|
|
11
|
+
from {
|
|
12
|
+
opacity: 0;
|
|
13
|
+
filter: blur(12px);
|
|
14
|
+
transform: translateY(12px);
|
|
15
|
+
}
|
|
16
|
+
to {
|
|
17
|
+
opacity: 1;
|
|
18
|
+
filter: blur(0);
|
|
19
|
+
transform: translateY(0);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.item {
|
|
24
|
+
animation: reveal 1.1s cubic-bezier(0.25, 1, 0.5, 1) both;
|
|
25
|
+
animation-delay: var(--reveal-delay, 0s);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (prefers-reduced-motion: reduce) {
|
|
29
|
+
.item {
|
|
30
|
+
animation: none;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,292 +1,66 @@
|
|
|
1
|
-
|
|
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"
|
|
1
|
+
import React, { type CSSProperties } from "react"
|
|
12
2
|
|
|
13
3
|
import { cn } from "~/lib/utils"
|
|
14
4
|
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
import styles from "./reveal.module.css"
|
|
6
|
+
|
|
7
|
+
export type PerType = "line" | "word"
|
|
17
8
|
|
|
18
9
|
export type TextEffectProps = {
|
|
19
10
|
as?: keyof React.JSX.IntrinsicElements
|
|
20
11
|
children: string
|
|
21
12
|
className?: string
|
|
22
|
-
|
|
13
|
+
/** Seconds before the first segment starts animating. */
|
|
23
14
|
delay?: number
|
|
24
|
-
|
|
25
|
-
onAnimationStart?: () => void
|
|
15
|
+
/** Split granularity for the stagger. Lines split on `\n`. */
|
|
26
16
|
per?: PerType
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
}
|
|
17
|
+
/** Seconds between each segment's start. */
|
|
18
|
+
stagger?: number
|
|
38
19
|
}
|
|
39
20
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
}
|
|
21
|
+
const defaultStagger: Record<PerType, number> = {
|
|
22
|
+
line: 0.15,
|
|
23
|
+
word: 0.04,
|
|
205
24
|
}
|
|
206
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Staggered blur + fade + rise text entrance, split per word or per line —
|
|
28
|
+
* pure CSS (see `reveal.module.css`), no animation-library dependency.
|
|
29
|
+
* Word segments keep their trailing space inside the animated span so text
|
|
30
|
+
* still wraps naturally.
|
|
31
|
+
*/
|
|
207
32
|
export function TextEffect({
|
|
208
33
|
as = "p",
|
|
209
34
|
children,
|
|
210
35
|
className,
|
|
211
|
-
containerTransition,
|
|
212
36
|
delay = 0,
|
|
213
|
-
onAnimationComplete,
|
|
214
|
-
onAnimationStart,
|
|
215
37
|
per = "word",
|
|
216
|
-
|
|
217
|
-
segmentTransition,
|
|
218
|
-
segmentWrapperClassName,
|
|
219
|
-
speedReveal = 1,
|
|
220
|
-
speedSegment = 1,
|
|
221
|
-
style,
|
|
222
|
-
trigger = true,
|
|
223
|
-
variants,
|
|
38
|
+
stagger,
|
|
224
39
|
}: TextEffectProps) {
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const
|
|
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
|
-
}
|
|
40
|
+
const Tag = as as React.ElementType
|
|
41
|
+
const segments =
|
|
42
|
+
per === "line" ? children.split("\n") : children.split(/(\s+)/).filter(Boolean)
|
|
43
|
+
const step = stagger ?? defaultStagger[per]
|
|
264
44
|
|
|
45
|
+
let animatedIndex = 0
|
|
265
46
|
return (
|
|
266
|
-
<
|
|
267
|
-
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
segmentWrapperClassName={segmentWrapperClassName}
|
|
285
|
-
variants={computedVariants.item}
|
|
286
|
-
/>
|
|
287
|
-
))}
|
|
288
|
-
</MotionTag>
|
|
289
|
-
)}
|
|
290
|
-
</AnimatePresence>
|
|
47
|
+
<Tag className={className}>
|
|
48
|
+
{segments.map((segment, index) => {
|
|
49
|
+
if (per === "word" && /^\s+$/.test(segment)) {
|
|
50
|
+
return <React.Fragment key={index}>{segment}</React.Fragment>
|
|
51
|
+
}
|
|
52
|
+
const segmentDelay = delay + animatedIndex * step
|
|
53
|
+
animatedIndex += 1
|
|
54
|
+
return (
|
|
55
|
+
<span
|
|
56
|
+
className={cn(styles.item, per === "line" ? "block" : "inline-block")}
|
|
57
|
+
key={index}
|
|
58
|
+
style={{ "--reveal-delay": `${segmentDelay}s` } as CSSProperties}
|
|
59
|
+
>
|
|
60
|
+
{segment}
|
|
61
|
+
</span>
|
|
62
|
+
)
|
|
63
|
+
})}
|
|
64
|
+
</Tag>
|
|
291
65
|
)
|
|
292
66
|
}
|
|
@@ -1,99 +1,78 @@
|
|
|
1
1
|
import type { Metadata } from "next"
|
|
2
2
|
|
|
3
|
-
import { buildSiteMetadata } from "@vexcms/core"
|
|
4
3
|
import { fetchQuery } from "convex/nextjs"
|
|
5
4
|
|
|
6
5
|
import { api } from "@convex/_generated/api"
|
|
7
6
|
import { anyApi } from "convex/server"
|
|
8
7
|
|
|
8
|
+
const TITLE_SUFFIX = " | Vex CMS"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves an `upload()` field's stored media id to a public URL via the
|
|
12
|
+
* Convex file storage adapter. Returns `undefined` when the media document
|
|
13
|
+
* no longer exists or the deployment is unreachable.
|
|
14
|
+
*/
|
|
15
|
+
async function resolveMediaUrl(mediaId: string): Promise<string | undefined> {
|
|
16
|
+
try {
|
|
17
|
+
const result = (await fetchQuery(anyApi.vex.media.getUrl, {
|
|
18
|
+
adapter: "convex",
|
|
19
|
+
mediaId,
|
|
20
|
+
})) as { url?: string; error?: string }
|
|
21
|
+
return result.url
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
/**
|
|
10
28
|
* Generate Next.js Metadata for a page.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
29
|
+
*
|
|
30
|
+
* Fetches site settings and, when a slug is given, the matching page
|
|
31
|
+
* document, then merges them — page-level `metaTitle`/`metaDescription`/
|
|
32
|
+
* `ogImage` win over the site's defaults from `siteSettings`.
|
|
13
33
|
*
|
|
14
34
|
* @param props.slug - Optional page slug to fetch per-page SEO overrides
|
|
15
35
|
*/
|
|
16
|
-
export async function generatePageMetadata(props: {
|
|
17
|
-
slug?: string
|
|
18
|
-
}): Promise<Metadata> {
|
|
36
|
+
export async function generatePageMetadata(props: { slug?: string }): Promise<Metadata> {
|
|
19
37
|
try {
|
|
20
|
-
|
|
21
|
-
const settings = (await fetchQuery(api.siteSettings.get)) as Record<
|
|
22
|
-
string,
|
|
23
|
-
unknown
|
|
24
|
-
> | null
|
|
25
|
-
|
|
38
|
+
const settings = (await fetchQuery(api.siteSettings.get)) as Record<string, unknown> | null
|
|
26
39
|
if (!settings) {
|
|
27
40
|
return { title: "Untitled" }
|
|
28
41
|
}
|
|
29
42
|
|
|
30
|
-
|
|
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
|
|
43
|
+
let pageData: Record<string, unknown> | undefined
|
|
50
44
|
if (props.slug) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
const pages = (await fetchQuery(api.pages.getBySlug, { slug: props.slug })) as
|
|
46
|
+
| Record<string, unknown>[]
|
|
47
|
+
| undefined
|
|
48
|
+
pageData = pages?.[0]
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
})
|
|
51
|
+
const pageTitle = (pageData?.metaTitle as string | undefined) ?? (pageData?.title as string | undefined)
|
|
52
|
+
const siteName = settings.name as string | undefined
|
|
53
|
+
const title = (pageTitle ?? (settings.metaTitle as string | undefined) ?? siteName ?? "Untitled") + TITLE_SUFFIX
|
|
54
|
+
const description =
|
|
55
|
+
(pageData?.metaDescription as string | undefined) ??
|
|
56
|
+
(settings.metaDescription as string | undefined) ??
|
|
57
|
+
(settings.description as string | undefined)
|
|
77
58
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
59
|
+
// `upload()` fields always store an array of media ids — the first entry
|
|
60
|
+
// is the selection. Page-level ogImage wins over the site default.
|
|
61
|
+
const pageOgImageId = (pageData?.ogImage as string[] | undefined)?.[0]
|
|
62
|
+
const siteOgImageId = (settings.ogImage as string[] | undefined)?.[0]
|
|
63
|
+
const ogImageId = pageOgImageId ?? siteOgImageId
|
|
64
|
+
const ogImageUrl = ogImageId ? await resolveMediaUrl(ogImageId) : undefined
|
|
65
|
+
|
|
66
|
+
const twitterHandle = settings.twitterHandle as string | undefined
|
|
67
|
+
|
|
68
|
+
const metadata: Metadata = { title, description }
|
|
83
69
|
|
|
84
|
-
if (
|
|
85
|
-
metadata.openGraph = {
|
|
86
|
-
title: meta.title,
|
|
87
|
-
description: meta.description,
|
|
88
|
-
images: [{ url: meta.ogImage }],
|
|
89
|
-
}
|
|
70
|
+
if (ogImageUrl) {
|
|
71
|
+
metadata.openGraph = { title, description, images: [{ url: ogImageUrl }] }
|
|
90
72
|
}
|
|
91
73
|
|
|
92
|
-
if (
|
|
93
|
-
metadata.twitter = {
|
|
94
|
-
card: "summary_large_image",
|
|
95
|
-
site: meta.twitterHandle,
|
|
96
|
-
}
|
|
74
|
+
if (twitterHandle) {
|
|
75
|
+
metadata.twitter = { card: "summary_large_image", site: twitterHandle }
|
|
97
76
|
}
|
|
98
77
|
|
|
99
78
|
return metadata
|