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.
Files changed (63) hide show
  1. package/package.json +1 -1
  2. package/templates/base-nextjs/convex/vex/globals.ts +82 -0
  3. package/templates/base-nextjs/convex/vex/versions.ts +66 -6
  4. package/templates/base-nextjs/package.json +7 -7
  5. package/templates/base-nextjs/src/app/admin/layout.tsx +29 -11
  6. package/templates/base-nextjs/src/db/constants/auth.ts +1 -3
  7. package/templates/base-nextjs/src/vexcms/access.ts +29 -13
  8. package/templates/base-nextjs/src/vexcms/collections/users.ts +2 -2
  9. package/templates/base-nextjs/vex.config.ts +2 -0
  10. package/templates/marketing-site/convex/footers.ts +14 -0
  11. package/templates/marketing-site/convex/headers.ts +14 -0
  12. package/templates/marketing-site/convex/siteSettings.ts +14 -0
  13. package/templates/marketing-site/convex/theme.ts +75 -0
  14. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  15. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +74 -0
  16. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +19 -51
  17. package/templates/marketing-site/src/app/(frontend)/layout.tsx +37 -0
  18. package/templates/marketing-site/src/app/(frontend)/page.tsx +24 -0
  19. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +55 -0
  20. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +19 -63
  21. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +18 -0
  22. package/templates/marketing-site/src/components/SiteFooter.tsx +28 -0
  23. package/templates/marketing-site/src/components/SiteHeader.tsx +28 -0
  24. package/templates/marketing-site/src/components/ThemeInjector.tsx +128 -0
  25. package/templates/marketing-site/src/components/ThemeStyle.tsx +104 -0
  26. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +134 -0
  27. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +174 -0
  28. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +33 -0
  29. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +138 -0
  30. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +292 -0
  31. package/templates/marketing-site/src/components/ui/accordion.tsx +74 -0
  32. package/templates/marketing-site/src/lib/colorConvert.ts +171 -0
  33. package/templates/marketing-site/src/lib/metadata.ts +104 -0
  34. package/templates/marketing-site/src/vexcms/access.ts +28 -0
  35. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +36 -0
  36. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +46 -0
  37. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +70 -0
  38. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +71 -0
  39. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +61 -0
  40. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +73 -0
  41. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +65 -0
  42. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +71 -0
  43. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +65 -0
  44. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +174 -0
  45. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +52 -0
  46. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +152 -0
  47. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +68 -0
  48. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +74 -0
  49. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +154 -0
  50. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +122 -0
  51. package/templates/marketing-site/src/vexcms/blocks/config.ts +40 -0
  52. package/templates/marketing-site/src/vexcms/blocks/constants.ts +9 -0
  53. package/templates/marketing-site/src/vexcms/blocks/index.ts +39 -0
  54. package/templates/marketing-site/src/vexcms/collections/footers.ts +11 -8
  55. package/templates/marketing-site/src/vexcms/collections/headers.ts +10 -14
  56. package/templates/marketing-site/src/vexcms/collections/index.ts +0 -1
  57. package/templates/marketing-site/src/vexcms/collections/pages.ts +30 -4
  58. package/templates/marketing-site/src/vexcms/collections/themes.ts +107 -14
  59. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -0
  60. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +57 -0
  61. package/templates/marketing-site/vex.config.ts +5 -5
  62. package/templates/base-nextjs/src/auth/permissions.ts +0 -102
  63. 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
+ &copy; {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
+ })