create-vexcms 0.0.8 → 0.0.10
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 +7 -7
- 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/admin/ThemeImport.tsx +174 -0
- package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +33 -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/components/ui/accordion.tsx +74 -0
- package/templates/marketing-site/src/lib/colorConvert.ts +171 -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,65 @@
|
|
|
1
|
+
import { array, defineBlock, object, text, upload } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import IconPickerField from "~/components/admin/IconPickerField"
|
|
4
|
+
import { TABLE_SLUG_MEDIA } from "~/db/constants"
|
|
5
|
+
import { BLOCK_SLUG_FOOTER } from "../constants"
|
|
6
|
+
|
|
7
|
+
export const footerBlock = defineBlock({
|
|
8
|
+
slug: BLOCK_SLUG_FOOTER,
|
|
9
|
+
label: "Footer",
|
|
10
|
+
fields: {
|
|
11
|
+
logoText: text({
|
|
12
|
+
label: "Logo Text",
|
|
13
|
+
defaultValue: "Vex CMS",
|
|
14
|
+
}),
|
|
15
|
+
logoImage: upload({
|
|
16
|
+
label: "Logo Image",
|
|
17
|
+
to: TABLE_SLUG_MEDIA,
|
|
18
|
+
}),
|
|
19
|
+
copyright: text({
|
|
20
|
+
label: "Copyright Text",
|
|
21
|
+
defaultValue: "Vex CMS. All rights reserved.",
|
|
22
|
+
}),
|
|
23
|
+
links: array({
|
|
24
|
+
label: "Links",
|
|
25
|
+
items: object({
|
|
26
|
+
fields: {
|
|
27
|
+
label: text({ label: "Label", required: true }),
|
|
28
|
+
href: text({ label: "Link", required: true }),
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
defaultValue: [
|
|
32
|
+
{ label: "Features", href: "/features" },
|
|
33
|
+
{ label: "Pricing", href: "/pricing" },
|
|
34
|
+
{ label: "Roadmap", href: "/roadmap" },
|
|
35
|
+
{ label: "Documentation", href: "/docs" },
|
|
36
|
+
{ label: "GitHub", href: "https://github.com/vexcms/vex" },
|
|
37
|
+
{ label: "npm", href: "https://www.npmjs.com/package/@vexcms/core" },
|
|
38
|
+
{ label: "Convex", href: "https://convex.dev" },
|
|
39
|
+
],
|
|
40
|
+
}),
|
|
41
|
+
socialLinks: array({
|
|
42
|
+
label: "Social Links",
|
|
43
|
+
items: object({
|
|
44
|
+
fields: {
|
|
45
|
+
platform: text({ label: "Platform", required: true }),
|
|
46
|
+
href: text({ label: "URL", required: true }),
|
|
47
|
+
icon: text({
|
|
48
|
+
label: "Icon",
|
|
49
|
+
admin: {
|
|
50
|
+
description: "Lucide icon name",
|
|
51
|
+
components: { Field: IconPickerField },
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
defaultValue: [
|
|
57
|
+
{ platform: "GitHub", href: "https://github.com/vexcms/vex", icon: "Github" },
|
|
58
|
+
{ platform: "X", href: "https://x.com/vexcms", icon: "Twitter" },
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
admin: {
|
|
63
|
+
icon: "panel-bottom",
|
|
64
|
+
},
|
|
65
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { BlockComponentProps } from "@vexcms/ui"
|
|
2
|
+
|
|
3
|
+
import { icons } from "lucide-react"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
|
|
6
|
+
export { footerBlock } from "./config"
|
|
7
|
+
|
|
8
|
+
function SocialIcon({ name }: { name: string }) {
|
|
9
|
+
const Icon = icons[name as keyof typeof icons]
|
|
10
|
+
if (!Icon) return null
|
|
11
|
+
return <Icon className="size-5" />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function FooterBlock({ block }: BlockComponentProps) {
|
|
15
|
+
const { logoText, copyright, links, socialLinks } = block as {
|
|
16
|
+
logoText?: string
|
|
17
|
+
logoImage?: string
|
|
18
|
+
copyright?: string
|
|
19
|
+
links?: Array<{ label: string; href: string }>
|
|
20
|
+
socialLinks?: Array<{ platform: string; href: string; icon?: string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<footer>
|
|
25
|
+
<div className="mx-auto max-w-5xl px-6 py-8 md:py-16">
|
|
26
|
+
<Link
|
|
27
|
+
aria-label="go home"
|
|
28
|
+
className="mx-auto block w-fit text-lg font-semibold"
|
|
29
|
+
href="/"
|
|
30
|
+
>
|
|
31
|
+
{logoText ?? "Vex CMS"}
|
|
32
|
+
</Link>
|
|
33
|
+
|
|
34
|
+
<div className="mt-6 flex flex-wrap justify-center gap-6 text-sm">
|
|
35
|
+
{(links ?? []).map((link, index) => (
|
|
36
|
+
<Link
|
|
37
|
+
className="text-muted-foreground hover:text-primary block duration-150"
|
|
38
|
+
href={link.href}
|
|
39
|
+
key={index}
|
|
40
|
+
>
|
|
41
|
+
<span>{link.label}</span>
|
|
42
|
+
</Link>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{socialLinks && socialLinks.length > 0 && (
|
|
47
|
+
<div className="my-8 flex flex-wrap justify-center gap-6 text-sm">
|
|
48
|
+
{socialLinks.map((social, index) => (
|
|
49
|
+
<Link
|
|
50
|
+
key={index}
|
|
51
|
+
href={social.href}
|
|
52
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
53
|
+
aria-label={social.platform}
|
|
54
|
+
>
|
|
55
|
+
{social.icon ? (
|
|
56
|
+
<SocialIcon name={social.icon} />
|
|
57
|
+
) : (
|
|
58
|
+
<span className="text-sm">{social.platform}</span>
|
|
59
|
+
)}
|
|
60
|
+
</Link>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<span className="text-muted-foreground block text-center text-sm">
|
|
66
|
+
© {new Date().getFullYear()} {copyright ?? "All rights reserved"}
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</footer>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { array, defineBlock, object, select, text, upload } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import { TABLE_SLUG_MEDIA } from "~/db/constants"
|
|
4
|
+
import { BLOCK_SLUG_HEADER } from "../constants"
|
|
5
|
+
|
|
6
|
+
export const headerBlock = defineBlock({
|
|
7
|
+
slug: BLOCK_SLUG_HEADER,
|
|
8
|
+
label: "Header",
|
|
9
|
+
fields: {
|
|
10
|
+
logoText: text({
|
|
11
|
+
label: "Logo Text",
|
|
12
|
+
defaultValue: "Vex CMS",
|
|
13
|
+
admin: { description: "Text to display as the logo" },
|
|
14
|
+
}),
|
|
15
|
+
logoImage: upload({
|
|
16
|
+
label: "Logo Image",
|
|
17
|
+
to: TABLE_SLUG_MEDIA,
|
|
18
|
+
admin: { description: "Image to display as the logo (overrides text)" },
|
|
19
|
+
}),
|
|
20
|
+
logoHref: text({
|
|
21
|
+
label: "Logo Link",
|
|
22
|
+
defaultValue: "/",
|
|
23
|
+
}),
|
|
24
|
+
menuItems: array({
|
|
25
|
+
label: "Menu Items",
|
|
26
|
+
items: object({
|
|
27
|
+
fields: {
|
|
28
|
+
label: text({ label: "Label", required: true }),
|
|
29
|
+
href: text({ label: "Link", required: true }),
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
defaultValue: [
|
|
33
|
+
{ label: "Features", href: "/features" },
|
|
34
|
+
{ label: "Pricing", href: "/pricing" },
|
|
35
|
+
{ label: "Roadmap", href: "/roadmap" },
|
|
36
|
+
{ label: "Docs", href: "/docs" },
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
actionButtons: array({
|
|
40
|
+
label: "Action Buttons",
|
|
41
|
+
items: object({
|
|
42
|
+
fields: {
|
|
43
|
+
label: text({ label: "Button Label", required: true }),
|
|
44
|
+
href: text({ label: "Button Link", required: true }),
|
|
45
|
+
variant: select({
|
|
46
|
+
label: "Variant",
|
|
47
|
+
options: [
|
|
48
|
+
{ label: "Default", value: "default" },
|
|
49
|
+
{ label: "Outline", value: "outline" },
|
|
50
|
+
{ label: "Ghost", value: "ghost" },
|
|
51
|
+
],
|
|
52
|
+
defaultValue: "default",
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
defaultValue: [
|
|
57
|
+
{ label: "GitHub", href: "https://github.com/vexcms/vex", variant: "ghost" },
|
|
58
|
+
{ label: "Get Started", href: "/docs", variant: "default" },
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
admin: {
|
|
63
|
+
icon: "panel-top",
|
|
64
|
+
},
|
|
65
|
+
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { BlockComponentProps } from "@vexcms/ui"
|
|
4
|
+
|
|
5
|
+
import { Menu, Settings, X } from "lucide-react"
|
|
6
|
+
import Link from "next/link"
|
|
7
|
+
import { useEffect, useState } from "react"
|
|
8
|
+
|
|
9
|
+
import { useSession } from "~/auth/client"
|
|
10
|
+
import { cn } from "~/lib/utils"
|
|
11
|
+
import { buttonVariants } from "~/components/ui/button"
|
|
12
|
+
|
|
13
|
+
export { headerBlock } from "./config"
|
|
14
|
+
|
|
15
|
+
export default function HeaderBlock({ block }: BlockComponentProps) {
|
|
16
|
+
const {
|
|
17
|
+
logoText,
|
|
18
|
+
logoHref = "/",
|
|
19
|
+
menuItems,
|
|
20
|
+
actionButtons,
|
|
21
|
+
} = block as {
|
|
22
|
+
logoText?: string
|
|
23
|
+
logoImage?: string
|
|
24
|
+
logoHref?: string
|
|
25
|
+
menuItems?: Array<{ label: string; href: string }>
|
|
26
|
+
actionButtons?: Array<{ label: string; href: string; variant?: string }>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { data: session } = useSession()
|
|
30
|
+
const isAdmin = session?.user?.role === "admin" ||
|
|
31
|
+
(Array.isArray((session?.user as any)?.role) && (session?.user as any)?.role?.includes("admin"))
|
|
32
|
+
|
|
33
|
+
const [menuState, setMenuState] = useState(false)
|
|
34
|
+
const [isScrolled, setIsScrolled] = useState(false)
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const handleScroll = () => {
|
|
38
|
+
setIsScrolled(window.scrollY > 50)
|
|
39
|
+
}
|
|
40
|
+
window.addEventListener("scroll", handleScroll)
|
|
41
|
+
return () => window.removeEventListener("scroll", handleScroll)
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<header>
|
|
46
|
+
<nav
|
|
47
|
+
className="fixed z-20 w-full px-2"
|
|
48
|
+
data-state={menuState ? "active" : undefined}
|
|
49
|
+
>
|
|
50
|
+
<div
|
|
51
|
+
className={cn(
|
|
52
|
+
"mx-auto mt-2 max-w-6xl px-6 transition-all duration-300 lg:px-12",
|
|
53
|
+
isScrolled &&
|
|
54
|
+
"bg-background/50 max-w-4xl rounded-2xl border backdrop-blur-lg lg:px-5"
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
<div className="relative flex flex-wrap items-center justify-between gap-6 py-3 lg:gap-0 lg:py-4">
|
|
58
|
+
<div className="flex w-full justify-between lg:w-auto">
|
|
59
|
+
<Link
|
|
60
|
+
aria-label="home"
|
|
61
|
+
className="flex items-center gap-2 text-lg font-semibold"
|
|
62
|
+
href={logoHref}
|
|
63
|
+
>
|
|
64
|
+
{logoText ?? "Vex CMS"}
|
|
65
|
+
</Link>
|
|
66
|
+
|
|
67
|
+
<button
|
|
68
|
+
aria-label={menuState ? "Close Menu" : "Open Menu"}
|
|
69
|
+
className="relative z-20 -m-2.5 -mr-4 block cursor-pointer p-2.5 lg:hidden"
|
|
70
|
+
onClick={() => setMenuState(!menuState)}
|
|
71
|
+
>
|
|
72
|
+
<Menu className="m-auto size-6 duration-200 in-data-[state=active]:scale-0 in-data-[state=active]:rotate-180 in-data-[state=active]:opacity-0" />
|
|
73
|
+
<X className="absolute inset-0 m-auto size-6 scale-0 -rotate-180 opacity-0 duration-200 in-data-[state=active]:scale-100 in-data-[state=active]:rotate-0 in-data-[state=active]:opacity-100" />
|
|
74
|
+
</button>
|
|
75
|
+
|
|
76
|
+
<div className="absolute inset-0 m-auto hidden size-fit lg:block">
|
|
77
|
+
<ul className="flex gap-8 text-sm">
|
|
78
|
+
{(menuItems ?? []).map((item, index) => (
|
|
79
|
+
<li key={index}>
|
|
80
|
+
<Link
|
|
81
|
+
className="text-muted-foreground hover:text-accent-foreground block duration-150"
|
|
82
|
+
href={item.href}
|
|
83
|
+
>
|
|
84
|
+
<span>{item.label}</span>
|
|
85
|
+
</Link>
|
|
86
|
+
</li>
|
|
87
|
+
))}
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="hidden lg:flex lg:gap-3">
|
|
93
|
+
{(actionButtons ?? []).map((button, index) => (
|
|
94
|
+
<Link
|
|
95
|
+
key={index}
|
|
96
|
+
href={button.href}
|
|
97
|
+
className={cn(
|
|
98
|
+
buttonVariants({
|
|
99
|
+
size: "sm",
|
|
100
|
+
variant: (button.variant as "default" | "outline" | "ghost") ?? "default",
|
|
101
|
+
})
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
<span>{button.label}</span>
|
|
105
|
+
</Link>
|
|
106
|
+
))}
|
|
107
|
+
{isAdmin && (
|
|
108
|
+
<Link
|
|
109
|
+
href="/admin"
|
|
110
|
+
className={cn(
|
|
111
|
+
buttonVariants({ size: "sm", variant: "outline" }),
|
|
112
|
+
"gap-1.5"
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
<Settings className="size-3.5" />
|
|
116
|
+
<span>Admin</span>
|
|
117
|
+
</Link>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Mobile menu */}
|
|
122
|
+
<div className="bg-background absolute top-[125%] z-10 mb-6 hidden w-full flex-wrap items-center justify-end space-y-8 rounded-3xl border p-6 shadow-2xl shadow-zinc-300/20 in-data-[state=active]:block md:flex-nowrap lg:m-0 lg:w-fit lg:space-y-0 lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none dark:shadow-none dark:lg:bg-transparent">
|
|
123
|
+
<div className="lg:hidden">
|
|
124
|
+
<ul className="space-y-6 text-base">
|
|
125
|
+
{(menuItems ?? []).map((item, index) => (
|
|
126
|
+
<li key={index}>
|
|
127
|
+
<Link
|
|
128
|
+
className="text-muted-foreground hover:text-accent-foreground block duration-150"
|
|
129
|
+
href={item.href}
|
|
130
|
+
>
|
|
131
|
+
<span>{item.label}</span>
|
|
132
|
+
</Link>
|
|
133
|
+
</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
|
|
137
|
+
{(actionButtons && actionButtons.length > 0 || isAdmin) && (
|
|
138
|
+
<div className="mt-6 flex flex-col space-y-3">
|
|
139
|
+
{(actionButtons ?? []).map((button, index) => (
|
|
140
|
+
<Link
|
|
141
|
+
key={index}
|
|
142
|
+
href={button.href}
|
|
143
|
+
className={cn(
|
|
144
|
+
buttonVariants({
|
|
145
|
+
size: "sm",
|
|
146
|
+
variant: (button.variant as "default" | "outline" | "ghost") ?? "default",
|
|
147
|
+
})
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
<span>{button.label}</span>
|
|
151
|
+
</Link>
|
|
152
|
+
))}
|
|
153
|
+
{isAdmin && (
|
|
154
|
+
<Link
|
|
155
|
+
href="/admin"
|
|
156
|
+
className={cn(
|
|
157
|
+
buttonVariants({ size: "sm", variant: "outline" }),
|
|
158
|
+
"gap-1.5"
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
<Settings className="size-3.5" />
|
|
162
|
+
<span>Admin</span>
|
|
163
|
+
</Link>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</nav>
|
|
172
|
+
</header>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineBlock, text } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import { BLOCK_SLUG_HERO } from "../constants"
|
|
4
|
+
|
|
5
|
+
export const heroBlock = defineBlock({
|
|
6
|
+
slug: BLOCK_SLUG_HERO,
|
|
7
|
+
label: "Hero Section",
|
|
8
|
+
fields: {
|
|
9
|
+
badgeText: text({
|
|
10
|
+
label: "Badge Text",
|
|
11
|
+
defaultValue: "Now in public beta",
|
|
12
|
+
admin: { description: "Small text shown in the announcement badge" },
|
|
13
|
+
}),
|
|
14
|
+
badgeLink: text({
|
|
15
|
+
label: "Badge Link",
|
|
16
|
+
defaultValue: "/docs",
|
|
17
|
+
admin: { description: "URL the badge links to" },
|
|
18
|
+
}),
|
|
19
|
+
heading: text({
|
|
20
|
+
label: "Heading",
|
|
21
|
+
required: true,
|
|
22
|
+
defaultValue: "The CMS built for Convex",
|
|
23
|
+
}),
|
|
24
|
+
subheading: text({
|
|
25
|
+
label: "Subheading",
|
|
26
|
+
defaultValue:
|
|
27
|
+
"Vex CMS gives you a full-featured content management system powered by Convex — real-time data, type-safe schemas, and a beautiful admin panel out of the box.",
|
|
28
|
+
}),
|
|
29
|
+
primaryCtaLabel: text({
|
|
30
|
+
label: "Primary CTA Label",
|
|
31
|
+
required: true,
|
|
32
|
+
defaultValue: "Get Started",
|
|
33
|
+
}),
|
|
34
|
+
primaryCtaHref: text({
|
|
35
|
+
label: "Primary CTA Link",
|
|
36
|
+
required: true,
|
|
37
|
+
defaultValue: "/docs",
|
|
38
|
+
}),
|
|
39
|
+
secondaryCtaLabel: text({
|
|
40
|
+
label: "Secondary CTA Label",
|
|
41
|
+
defaultValue: "View on GitHub",
|
|
42
|
+
}),
|
|
43
|
+
secondaryCtaHref: text({
|
|
44
|
+
label: "Secondary CTA Link",
|
|
45
|
+
defaultValue: "https://github.com/vexcms/vex",
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
admin: {
|
|
49
|
+
icon: "sparkles",
|
|
50
|
+
blockStyles: ["container", "text"],
|
|
51
|
+
},
|
|
52
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { BlockComponentProps } from "@vexcms/ui"
|
|
4
|
+
|
|
5
|
+
import { ArrowRight } from "lucide-react"
|
|
6
|
+
import Link from "next/link"
|
|
7
|
+
|
|
8
|
+
import { cn } from "~/lib/utils"
|
|
9
|
+
import { AnimatedGroup } from "~/components/motion-primitives/animated-group"
|
|
10
|
+
import { TextEffect } from "~/components/motion-primitives/text-effect"
|
|
11
|
+
import { buttonVariants } from "~/components/ui/button"
|
|
12
|
+
|
|
13
|
+
export { heroBlock } from "./config"
|
|
14
|
+
|
|
15
|
+
const transitionVariants = {
|
|
16
|
+
item: {
|
|
17
|
+
hidden: {
|
|
18
|
+
filter: "blur(12px)",
|
|
19
|
+
opacity: 0,
|
|
20
|
+
y: 12,
|
|
21
|
+
},
|
|
22
|
+
visible: {
|
|
23
|
+
filter: "blur(0px)",
|
|
24
|
+
opacity: 1,
|
|
25
|
+
transition: {
|
|
26
|
+
type: "spring",
|
|
27
|
+
bounce: 0.3,
|
|
28
|
+
duration: 1.5,
|
|
29
|
+
},
|
|
30
|
+
y: 0,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function HeroBlock({ block, blockStyles }: BlockComponentProps) {
|
|
36
|
+
const {
|
|
37
|
+
badgeText,
|
|
38
|
+
badgeLink,
|
|
39
|
+
heading,
|
|
40
|
+
subheading,
|
|
41
|
+
primaryCtaLabel,
|
|
42
|
+
primaryCtaHref,
|
|
43
|
+
secondaryCtaLabel,
|
|
44
|
+
secondaryCtaHref,
|
|
45
|
+
} = block as Record<string, string>
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={cn("overflow-hidden", blockStyles)}>
|
|
49
|
+
<div
|
|
50
|
+
aria-hidden
|
|
51
|
+
className="absolute inset-0 isolate hidden opacity-65 contain-strict lg:block"
|
|
52
|
+
>
|
|
53
|
+
<div className="absolute top-0 left-0 h-320 w-140 -translate-y-87.5 -rotate-45 rounded-full bg-[radial-gradient(68.54%_68.72%_at_55.02%_31.46%,hsla(0,0%,85%,.08)_0,hsla(0,0%,55%,.02)_50%,hsla(0,0%,45%,0)_80%)]" />
|
|
54
|
+
<div className="absolute top-0 left-0 h-320 w-60 [translate:5%_-50%] -rotate-45 rounded-full bg-[radial-gradient(50%_50%_at_50%_50%,hsla(0,0%,85%,.06)_0,hsla(0,0%,45%,.02)_80%,transparent_100%)]" />
|
|
55
|
+
<div className="absolute top-0 left-0 h-320 w-60 -translate-y-87.5 -rotate-45 bg-[radial-gradient(50%_50%_at_50%_50%,hsla(0,0%,85%,.04)_0,hsla(0,0%,45%,.02)_80%,transparent_100%)]" />
|
|
56
|
+
</div>
|
|
57
|
+
<section>
|
|
58
|
+
<div className="relative min-h-[90vh] flex flex-col items-center justify-center">
|
|
59
|
+
<div className="absolute inset-0 -z-10 size-full [background:radial-gradient(125%_125%_at_50%_100%,transparent_0%,var(--color-background)_75%)]" />
|
|
60
|
+
<div className="mx-auto max-w-7xl px-6">
|
|
61
|
+
<div className="text-center sm:mx-auto lg:mr-auto">
|
|
62
|
+
{badgeText && badgeLink && (
|
|
63
|
+
<AnimatedGroup
|
|
64
|
+
// @ts-expect-error variant type mismatch
|
|
65
|
+
variants={transitionVariants}
|
|
66
|
+
>
|
|
67
|
+
<Link
|
|
68
|
+
className="hover:bg-background dark:hover:border-t-border bg-muted group mx-auto flex w-fit items-center gap-4 rounded-full border p-1 pl-4 shadow-md shadow-zinc-950/5 transition-colors duration-300 dark:border-t-white/5 dark:shadow-zinc-950"
|
|
69
|
+
href={badgeLink}
|
|
70
|
+
>
|
|
71
|
+
<span className="text-foreground text-sm">
|
|
72
|
+
{badgeText}
|
|
73
|
+
</span>
|
|
74
|
+
<span className="dark:border-background block h-4 w-0.5 border-l bg-white dark:bg-zinc-700" />
|
|
75
|
+
<div className="bg-background group-hover:bg-muted size-6 overflow-hidden rounded-full duration-500">
|
|
76
|
+
<div className="flex w-12 -translate-x-1/2 duration-500 ease-in-out group-hover:translate-x-0">
|
|
77
|
+
<span className="flex size-6">
|
|
78
|
+
<ArrowRight className="m-auto size-3" />
|
|
79
|
+
</span>
|
|
80
|
+
<span className="flex size-6">
|
|
81
|
+
<ArrowRight className="m-auto size-3" />
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</Link>
|
|
86
|
+
</AnimatedGroup>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<TextEffect
|
|
90
|
+
as="h1"
|
|
91
|
+
className="mt-8 text-6xl text-balance md:text-7xl lg:mt-16 xl:text-[5.25rem]"
|
|
92
|
+
preset="fade-in-blur"
|
|
93
|
+
speedSegment={0.3}
|
|
94
|
+
>
|
|
95
|
+
{heading ?? ""}
|
|
96
|
+
</TextEffect>
|
|
97
|
+
<TextEffect
|
|
98
|
+
as="p"
|
|
99
|
+
className="mx-auto mt-8 max-w-2xl text-lg text-balance text-muted-foreground"
|
|
100
|
+
delay={0.5}
|
|
101
|
+
per="line"
|
|
102
|
+
preset="fade-in-blur"
|
|
103
|
+
speedSegment={0.3}
|
|
104
|
+
>
|
|
105
|
+
{subheading ?? ""}
|
|
106
|
+
</TextEffect>
|
|
107
|
+
|
|
108
|
+
<AnimatedGroup
|
|
109
|
+
className="mt-12 flex flex-col items-center justify-center gap-2 md:flex-row"
|
|
110
|
+
// @ts-expect-error variant type mismatch
|
|
111
|
+
variants={{
|
|
112
|
+
container: {
|
|
113
|
+
visible: {
|
|
114
|
+
transition: {
|
|
115
|
+
delayChildren: 0.75,
|
|
116
|
+
staggerChildren: 0.05,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
...transitionVariants,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<div className="bg-foreground/10 rounded-[calc(var(--radius-xl)+0.125rem)] border p-0.5">
|
|
124
|
+
<Link
|
|
125
|
+
href={primaryCtaHref ?? "/"}
|
|
126
|
+
className={cn(
|
|
127
|
+
buttonVariants({ size: "lg" }),
|
|
128
|
+
"rounded-xl px-5 text-base"
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
<span className="text-nowrap">{primaryCtaLabel}</span>
|
|
132
|
+
</Link>
|
|
133
|
+
</div>
|
|
134
|
+
{secondaryCtaLabel && (
|
|
135
|
+
<Link
|
|
136
|
+
href={secondaryCtaHref ?? "/"}
|
|
137
|
+
className={cn(
|
|
138
|
+
buttonVariants({ variant: "ghost", size: "lg" }),
|
|
139
|
+
"h-10.5 rounded-xl px-5"
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
<span className="text-nowrap">{secondaryCtaLabel}</span>
|
|
143
|
+
</Link>
|
|
144
|
+
)}
|
|
145
|
+
</AnimatedGroup>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</section>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { array, defineBlock, object, text } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import IconPickerField from "~/components/admin/IconPickerField"
|
|
4
|
+
import { BLOCK_SLUG_HOW_IT_WORKS } from "../constants"
|
|
5
|
+
|
|
6
|
+
export const howItWorksBlock = defineBlock({
|
|
7
|
+
slug: BLOCK_SLUG_HOW_IT_WORKS,
|
|
8
|
+
label: "How It Works",
|
|
9
|
+
fields: {
|
|
10
|
+
heading: text({
|
|
11
|
+
label: "Heading",
|
|
12
|
+
required: true,
|
|
13
|
+
defaultValue: "Get started in minutes",
|
|
14
|
+
}),
|
|
15
|
+
subheading: text({
|
|
16
|
+
label: "Subheading",
|
|
17
|
+
defaultValue:
|
|
18
|
+
"From zero to a fully functional CMS in four steps. No boilerplate, no config files to wrestle with.",
|
|
19
|
+
}),
|
|
20
|
+
steps: array({
|
|
21
|
+
label: "Steps",
|
|
22
|
+
required: true,
|
|
23
|
+
items: object({
|
|
24
|
+
fields: {
|
|
25
|
+
icon: text({
|
|
26
|
+
label: "Icon",
|
|
27
|
+
admin: {
|
|
28
|
+
description: "Lucide icon name",
|
|
29
|
+
components: { Field: IconPickerField },
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
title: text({ label: "Title", required: true }),
|
|
33
|
+
description: text({ label: "Description", required: true }),
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
defaultValue: [
|
|
37
|
+
{
|
|
38
|
+
icon: "Terminal",
|
|
39
|
+
title: "Scaffold your project",
|
|
40
|
+
description:
|
|
41
|
+
"Run npx create-vexcms@latest to get a Next.js app with Convex, authentication, and the admin panel pre-configured.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
icon: "Code",
|
|
45
|
+
title: "Define your schema",
|
|
46
|
+
description:
|
|
47
|
+
"Use defineCollection() and field helpers to declare your content model in TypeScript. Vex generates your Convex schema, types, and queries automatically.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
icon: "LayoutGrid",
|
|
51
|
+
title: "Build with blocks",
|
|
52
|
+
description:
|
|
53
|
+
"Compose pages from reusable content blocks. Each block is a React component with a typed config — drag, drop, and edit inline from the admin panel.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
icon: "Rocket",
|
|
57
|
+
title: "Deploy and go live",
|
|
58
|
+
description:
|
|
59
|
+
"Push to Convex and deploy your Next.js app. Real-time content updates flow to every connected client instantly — no cache invalidation needed.",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
admin: {
|
|
65
|
+
icon: "list-ordered",
|
|
66
|
+
blockStyles: ["container", "text"],
|
|
67
|
+
},
|
|
68
|
+
})
|