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,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
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { BlockComponentProps } from "@vexcms/ui"
|
|
4
|
+
|
|
5
|
+
import { icons } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "~/lib/utils"
|
|
8
|
+
|
|
9
|
+
export { howItWorksBlock } from "./config"
|
|
10
|
+
|
|
11
|
+
function LucideIcon({ name }: { name: string }) {
|
|
12
|
+
const Icon = icons[name as keyof typeof icons]
|
|
13
|
+
if (!Icon) return null
|
|
14
|
+
return <Icon className="size-5" />
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function HowItWorksBlock({
|
|
18
|
+
block,
|
|
19
|
+
blockStyles,
|
|
20
|
+
}: BlockComponentProps) {
|
|
21
|
+
const { heading, subheading, steps } = block as unknown as {
|
|
22
|
+
heading: string
|
|
23
|
+
subheading?: string
|
|
24
|
+
steps?: Array<{ icon?: string; title: string; description: string }>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<section className={cn("py-16 md:py-32", blockStyles)}>
|
|
29
|
+
<div className="mx-auto max-w-3xl px-6">
|
|
30
|
+
<div className="text-center">
|
|
31
|
+
<h2 className="text-4xl font-semibold text-balance lg:text-5xl">
|
|
32
|
+
{heading}
|
|
33
|
+
</h2>
|
|
34
|
+
{subheading && (
|
|
35
|
+
<p className="text-muted-foreground mt-4 text-balance">
|
|
36
|
+
{subheading}
|
|
37
|
+
</p>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="mt-16">
|
|
42
|
+
{(steps ?? []).map((step, index) => {
|
|
43
|
+
const isLast = index === (steps?.length ?? 1) - 1
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div key={index} className="flex flex-row gap-6">
|
|
47
|
+
<div className="flex flex-col items-center">
|
|
48
|
+
<div className="bg-primary text-primary-foreground flex size-10 shrink-0 items-center justify-center rounded-full">
|
|
49
|
+
{step.icon ? (
|
|
50
|
+
<LucideIcon name={step.icon} />
|
|
51
|
+
) : (
|
|
52
|
+
<span className="text-sm font-semibold">
|
|
53
|
+
{index + 1}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
{!isLast && (
|
|
58
|
+
<div className="bg-border w-px flex-1" />
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
<div className={cn("flex-1", !isLast && "pb-10")}>
|
|
62
|
+
<h3 className="text-lg font-semibold">{step.title}</h3>
|
|
63
|
+
<p className="text-muted-foreground mt-1">
|
|
64
|
+
{step.description}
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
})}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { array, defineBlock, object, select, text } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import { BLOCK_SLUG_ROADMAP } from "../constants"
|
|
4
|
+
|
|
5
|
+
export const roadmapBlock = defineBlock({
|
|
6
|
+
slug: BLOCK_SLUG_ROADMAP,
|
|
7
|
+
label: "Roadmap",
|
|
8
|
+
fields: {
|
|
9
|
+
heading: text({
|
|
10
|
+
label: "Heading",
|
|
11
|
+
required: true,
|
|
12
|
+
defaultValue: "Roadmap",
|
|
13
|
+
}),
|
|
14
|
+
subheading: text({
|
|
15
|
+
label: "Subheading",
|
|
16
|
+
defaultValue:
|
|
17
|
+
"What we've shipped and what's coming next. Vex CMS is actively developed — here's where we're headed.",
|
|
18
|
+
}),
|
|
19
|
+
items: array({
|
|
20
|
+
label: "Roadmap Items",
|
|
21
|
+
required: true,
|
|
22
|
+
items: object({
|
|
23
|
+
fields: {
|
|
24
|
+
feature: text({ label: "Feature Name", required: true }),
|
|
25
|
+
description: text({ label: "Description" }),
|
|
26
|
+
status: select({
|
|
27
|
+
label: "Status",
|
|
28
|
+
required: true,
|
|
29
|
+
options: [
|
|
30
|
+
{ label: "Shipped", value: "shipped" },
|
|
31
|
+
{ label: "Coming Soon", value: "coming-soon" },
|
|
32
|
+
{ label: "Planned", value: "planned" },
|
|
33
|
+
],
|
|
34
|
+
defaultValue: "shipped",
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
defaultValue: [
|
|
39
|
+
{
|
|
40
|
+
feature: "16 Field Types",
|
|
41
|
+
description:
|
|
42
|
+
"Text, number, select, date, relationship, upload, richtext, blocks, color, tabs, and more.",
|
|
43
|
+
status: "shipped",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
feature: "Admin Panel",
|
|
47
|
+
description:
|
|
48
|
+
"Full-featured admin UI with list views, edit forms, media library, and draft/publish workflow.",
|
|
49
|
+
status: "shipped",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
feature: "Real-Time Queries",
|
|
53
|
+
description:
|
|
54
|
+
"Every query is live via Convex. Content updates appear instantly across all connected clients.",
|
|
55
|
+
status: "shipped",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
feature: "Live Preview",
|
|
59
|
+
description:
|
|
60
|
+
"Side-by-side iframe preview with responsive breakpoints and real-time updates as you edit.",
|
|
61
|
+
status: "shipped",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
feature: "Block System",
|
|
65
|
+
description:
|
|
66
|
+
"Compose pages from reusable content blocks with drag-and-drop reordering and inline editing.",
|
|
67
|
+
status: "shipped",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
feature: "Authentication & RBAC",
|
|
71
|
+
description:
|
|
72
|
+
"Better Auth integration with role-based access control at the document and field level.",
|
|
73
|
+
status: "shipped",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
feature: "Rich Text Editor",
|
|
77
|
+
description:
|
|
78
|
+
"Plate.js-powered editor with media uploads, links, tables, and custom elements.",
|
|
79
|
+
status: "shipped",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
feature: "CLI & Scaffolding",
|
|
83
|
+
description:
|
|
84
|
+
"vex dev with watch/generate, and create-vexcms for instant project setup.",
|
|
85
|
+
status: "shipped",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
feature: "Theme System",
|
|
89
|
+
description:
|
|
90
|
+
"Database-driven themes with light/dark mode, CSS variables, and OKLCH color support.",
|
|
91
|
+
status: "shipped",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
feature: "Block Styles",
|
|
95
|
+
description:
|
|
96
|
+
"Per-block responsive styling with Tailwind presets — margin, padding, typography, layout, and more.",
|
|
97
|
+
status: "shipped",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
feature: "Content Scheduling",
|
|
101
|
+
description:
|
|
102
|
+
"Set a publishAt timestamp and content goes live automatically via Convex scheduled functions.",
|
|
103
|
+
status: "coming-soon",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
feature: "Team Management",
|
|
107
|
+
description:
|
|
108
|
+
"Invite users, assign roles, and manage pending invitations from the admin panel.",
|
|
109
|
+
status: "coming-soon",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
feature: "API Keys",
|
|
113
|
+
description:
|
|
114
|
+
"Read-only API tokens for external integrations with configurable rate limiting.",
|
|
115
|
+
status: "coming-soon",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
feature: "Audit Log",
|
|
119
|
+
description:
|
|
120
|
+
"Track who changed what and when across all collections and documents.",
|
|
121
|
+
status: "coming-soon",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
feature: "Environments",
|
|
125
|
+
description:
|
|
126
|
+
"Project-level content branching with staging and production environments and atomic promotion.",
|
|
127
|
+
status: "planned",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
feature: "Localization",
|
|
131
|
+
description:
|
|
132
|
+
"i18n field variants with per-locale versioning and content translation workflows.",
|
|
133
|
+
status: "planned",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
feature: "Approval Workflows",
|
|
137
|
+
description:
|
|
138
|
+
"Review and sign-off steps before content goes live. Configurable multi-step approval chains.",
|
|
139
|
+
status: "planned",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
feature: "Plugin System",
|
|
143
|
+
description:
|
|
144
|
+
"Extend VEX with community plugins for custom fields, integrations, and admin panel features.",
|
|
145
|
+
status: "planned",
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
admin: {
|
|
151
|
+
icon: "map",
|
|
152
|
+
blockStyles: ["container"],
|
|
153
|
+
},
|
|
154
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { BlockComponentProps } from "@vexcms/ui"
|
|
4
|
+
|
|
5
|
+
import { Check, Clock, Compass } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "~/lib/utils"
|
|
8
|
+
|
|
9
|
+
export { roadmapBlock } from "./config"
|
|
10
|
+
|
|
11
|
+
const statusConfig = {
|
|
12
|
+
shipped: {
|
|
13
|
+
label: "Shipped",
|
|
14
|
+
icon: Check,
|
|
15
|
+
badgeClass:
|
|
16
|
+
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
|
|
17
|
+
},
|
|
18
|
+
"coming-soon": {
|
|
19
|
+
label: "Coming Soon",
|
|
20
|
+
icon: Clock,
|
|
21
|
+
badgeClass:
|
|
22
|
+
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
|
|
23
|
+
},
|
|
24
|
+
planned: {
|
|
25
|
+
label: "Planned",
|
|
26
|
+
icon: Compass,
|
|
27
|
+
badgeClass:
|
|
28
|
+
"bg-zinc-500/10 text-zinc-600 dark:text-zinc-400 border-zinc-500/20",
|
|
29
|
+
},
|
|
30
|
+
} as const
|
|
31
|
+
|
|
32
|
+
type RoadmapItem = {
|
|
33
|
+
feature: string
|
|
34
|
+
description?: string
|
|
35
|
+
status: "shipped" | "coming-soon" | "planned"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type StatusKey = keyof typeof statusConfig
|
|
39
|
+
|
|
40
|
+
const statusOrder: StatusKey[] = ["shipped", "coming-soon", "planned"]
|
|
41
|
+
|
|
42
|
+
export default function RoadmapBlock({
|
|
43
|
+
block,
|
|
44
|
+
blockStyles,
|
|
45
|
+
}: BlockComponentProps) {
|
|
46
|
+
const { heading, subheading, items } = block as unknown as {
|
|
47
|
+
heading: string
|
|
48
|
+
subheading?: string
|
|
49
|
+
items?: RoadmapItem[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const grouped = statusOrder
|
|
53
|
+
.map((status) => ({
|
|
54
|
+
status,
|
|
55
|
+
config: statusConfig[status],
|
|
56
|
+
items: (items ?? []).filter(
|
|
57
|
+
(item) => item.status === status && item.status in statusConfig
|
|
58
|
+
),
|
|
59
|
+
}))
|
|
60
|
+
.filter((group) => group.items.length > 0)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<section className={cn("py-16 md:py-32", blockStyles)}>
|
|
64
|
+
<div className="mx-auto max-w-5xl px-6">
|
|
65
|
+
<div className="text-center">
|
|
66
|
+
<h2 className="text-4xl font-semibold text-balance lg:text-5xl">
|
|
67
|
+
{heading}
|
|
68
|
+
</h2>
|
|
69
|
+
{subheading && (
|
|
70
|
+
<p className="text-muted-foreground mt-4 text-balance">
|
|
71
|
+
{subheading}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{grouped.map((group, groupIndex) => {
|
|
77
|
+
const StatusIcon = group.config.icon
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div key={group.status} className={groupIndex === 0 ? "mt-16" : "mt-12"}>
|
|
81
|
+
<div className="flex items-center gap-2">
|
|
82
|
+
<StatusIcon className="size-5" />
|
|
83
|
+
<h3 className="text-lg font-semibold">{group.config.label}</h3>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
87
|
+
{group.items.map((item, itemIndex) => {
|
|
88
|
+
const BadgeIcon = group.config.icon
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
key={itemIndex}
|
|
93
|
+
className="bg-card rounded-xl border p-5"
|
|
94
|
+
>
|
|
95
|
+
<div className="flex items-center justify-between gap-4">
|
|
96
|
+
<span className="font-medium">{item.feature}</span>
|
|
97
|
+
<span
|
|
98
|
+
className={cn(
|
|
99
|
+
"inline-flex shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium",
|
|
100
|
+
group.config.badgeClass
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
<BadgeIcon className="size-3" />
|
|
104
|
+
{group.config.label}
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
{item.description && (
|
|
108
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
109
|
+
{item.description}
|
|
110
|
+
</p>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
})}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
</section>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Block configs only — no React/motion dependencies
|
|
2
|
+
// Safe to import from collections and server-side code
|
|
3
|
+
import { heroBlock } from "./Hero/config"
|
|
4
|
+
import { featuresBlock } from "./Features/config"
|
|
5
|
+
import { ctaBlock } from "./CTA/config"
|
|
6
|
+
import { faqBlock } from "./FAQ/config"
|
|
7
|
+
import { headerBlock } from "./Header/config"
|
|
8
|
+
import { footerBlock } from "./Footer/config"
|
|
9
|
+
import { howItWorksBlock } from "./HowItWorks/config"
|
|
10
|
+
import { roadmapBlock } from "./Roadmap/config"
|
|
11
|
+
|
|
12
|
+
/** Page content block definitions */
|
|
13
|
+
export const pageBlocks = [
|
|
14
|
+
heroBlock,
|
|
15
|
+
featuresBlock,
|
|
16
|
+
howItWorksBlock,
|
|
17
|
+
roadmapBlock,
|
|
18
|
+
ctaBlock,
|
|
19
|
+
faqBlock,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
/** Header block definitions */
|
|
23
|
+
export const headerBlocks = [headerBlock]
|
|
24
|
+
|
|
25
|
+
/** Footer block definitions */
|
|
26
|
+
export const footerBlocks = [footerBlock]
|
|
27
|
+
|
|
28
|
+
/** All block definitions combined */
|
|
29
|
+
export const allBlocks = [...pageBlocks, ...headerBlocks, ...footerBlocks]
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
heroBlock,
|
|
33
|
+
featuresBlock,
|
|
34
|
+
ctaBlock,
|
|
35
|
+
faqBlock,
|
|
36
|
+
headerBlock,
|
|
37
|
+
footerBlock,
|
|
38
|
+
howItWorksBlock,
|
|
39
|
+
roadmapBlock,
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Block slugs
|
|
2
|
+
export const BLOCK_SLUG_HERO = "hero" as const
|
|
3
|
+
export const BLOCK_SLUG_FEATURES = "features" as const
|
|
4
|
+
export const BLOCK_SLUG_CTA = "cta" as const
|
|
5
|
+
export const BLOCK_SLUG_FAQ = "faq" as const
|
|
6
|
+
export const BLOCK_SLUG_HEADER = "header" as const
|
|
7
|
+
export const BLOCK_SLUG_FOOTER = "footer" as const
|
|
8
|
+
export const BLOCK_SLUG_HOW_IT_WORKS = "how_it_works" as const
|
|
9
|
+
export const BLOCK_SLUG_ROADMAP = "roadmap" as const
|