create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.21
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/README.md +48 -14
- package/dist/index.js +1714 -1504
- package/package.json +2 -2
- package/templates/base-nextjs/README.md +18 -5
- package/templates/base-nextjs/convex/auth/index.ts +2 -2
- package/templates/base-nextjs/convex/auth/options.ts +27 -26
- package/templates/base-nextjs/convex/auth/schema.ts +51 -0
- package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
- package/templates/base-nextjs/convex/tsconfig.json +2 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
- package/templates/base-nextjs/convex/vex/globals.ts +3 -2
- package/templates/base-nextjs/convex/vex/media.ts +3 -2
- package/templates/base-nextjs/convex/vex.schema.ts +1 -0
- package/templates/base-nextjs/convex/vex.ts +17 -4
- package/templates/base-nextjs/next.config.ts +52 -6
- package/templates/base-nextjs/package.json +18 -7
- package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
- package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
- package/templates/base-nextjs/src/app/globals.css +10 -9
- package/templates/base-nextjs/src/app/layout.tsx +6 -0
- package/templates/base-nextjs/src/auth/access.ts +14 -2
- package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
- package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
- package/templates/base-nextjs/src/components/component-example.tsx +1 -1
- package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
- package/templates/base-nextjs/src/vex.config.server.ts +30 -0
- package/templates/base-nextjs/src/vex.config.ts +14 -11
- package/templates/base-nextjs/src/vex.types.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
- package/templates/marketing-site/convex/pages.ts +20 -1
- package/templates/marketing-site/convex/schema.ts +4 -4
- package/templates/marketing-site/convex/seed.ts +515 -70
- package/templates/marketing-site/convex/siteSettings.ts +1 -1
- package/templates/marketing-site/convex/theme.ts +3 -3
- package/templates/marketing-site/convex/vex.schema.ts +75 -38
- package/templates/marketing-site/public/favicon.svg +14 -0
- package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
- package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
- package/templates/marketing-site/src/app/globals.css +413 -0
- package/templates/marketing-site/src/app/layout.tsx +85 -0
- package/templates/marketing-site/src/app/robots.ts +13 -0
- package/templates/marketing-site/src/app/sitemap.ts +18 -0
- package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
- package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
- package/templates/marketing-site/src/components/CodePane.tsx +98 -0
- package/templates/marketing-site/src/components/Container.tsx +25 -0
- package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
- package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
- package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
- package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
- package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
- package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
- package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
- package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
- package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
- 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 +47 -266
- package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
- package/templates/marketing-site/src/lib/highlight.ts +134 -0
- package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
- package/templates/marketing-site/src/lib/metadata.ts +61 -33
- package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
- package/templates/marketing-site/src/lib/vex.ts +18 -0
- package/templates/marketing-site/src/proxy.ts +179 -0
- package/templates/marketing-site/src/vex.config.server.ts +27 -0
- package/templates/marketing-site/src/vex.config.ts +27 -9
- package/templates/marketing-site/src/vex.types.ts +102 -76
- package/templates/marketing-site/src/vexcms/api.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
- package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
- package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
- package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
- package/dist/index.mjs +0 -30976
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
- package/templates/marketing-site/src/app/(frontend)/page.tsx +0 -21
|
@@ -1,71 +1,84 @@
|
|
|
1
1
|
import type { BlockComponentProps } from "@vexcms/react"
|
|
2
2
|
|
|
3
|
-
import Link from "next/link"
|
|
4
|
-
|
|
5
3
|
import {
|
|
6
4
|
Accordion,
|
|
7
5
|
AccordionContent,
|
|
8
6
|
AccordionItem,
|
|
9
7
|
AccordionTrigger,
|
|
8
|
+
buttonVariants,
|
|
9
|
+
cn,
|
|
10
10
|
} from "@vexcms/react"
|
|
11
|
+
import { ArrowRight } from "lucide-react"
|
|
12
|
+
import Link from "next/link"
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
import type { FaqBlock } from "~/vex.types"
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
const { heading, subheading, supportLink, items } = block as unknown as {
|
|
16
|
-
heading: string
|
|
17
|
-
subheading?: string
|
|
18
|
-
supportLink?: string
|
|
19
|
-
items?: Array<{ question: string; answer: string }>
|
|
20
|
-
}
|
|
16
|
+
import { Container } from "~/components/Container"
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
<section className="py-16 md:py-24">
|
|
24
|
-
<div className="mx-auto max-w-5xl px-4 md:px-6">
|
|
25
|
-
<div className="mx-auto max-w-xl text-center">
|
|
26
|
-
<h2 className="text-3xl font-bold text-balance md:text-4xl lg:text-5xl">
|
|
27
|
-
{heading}
|
|
28
|
-
</h2>
|
|
29
|
-
{subheading && (
|
|
30
|
-
<p className="text-muted-foreground mt-4 text-balance">
|
|
31
|
-
{subheading}
|
|
32
|
-
</p>
|
|
33
|
-
)}
|
|
34
|
-
</div>
|
|
18
|
+
export { faqBlock } from "./config"
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<hr className="mx-7 border-dashed group-last:hidden" />
|
|
52
|
-
</div>
|
|
53
|
-
))}
|
|
54
|
-
</Accordion>
|
|
20
|
+
/**
|
|
21
|
+
* FAQ — question left, answer under it.
|
|
22
|
+
*
|
|
23
|
+
* Two columns rather than the sourced centred layout: it gives the support
|
|
24
|
+
* link a home, holds answers at a 68ch measure instead of 90ch, and stops the
|
|
25
|
+
* centred-heading pattern appearing three times on one page.
|
|
26
|
+
*
|
|
27
|
+
* Items are rules, not cards — no radius, no background, no shadow, which is
|
|
28
|
+
* the only treatment that reads the same in both themes.
|
|
29
|
+
*/
|
|
30
|
+
export default function FAQBlockRenderer({ block }: BlockComponentProps) {
|
|
31
|
+
const { heading, items, subheading, supportLink } = block as FaqBlock
|
|
32
|
+
const faqs = items ?? []
|
|
33
|
+
|
|
34
|
+
if (faqs.length === 0) {return null}
|
|
55
35
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
36
|
+
return (
|
|
37
|
+
<section className="py-14 md:py-20 xl:py-28">
|
|
38
|
+
<Container>
|
|
39
|
+
<div className="grid gap-8 xl:grid-cols-[4fr_7fr] xl:items-start xl:gap-14">
|
|
40
|
+
<div>
|
|
41
|
+
<h2 className="text-[26px] leading-[1.18] font-bold tracking-[-0.025em] text-balance text-foreground md:text-3xl xl:text-4xl">
|
|
42
|
+
{heading}
|
|
43
|
+
</h2>
|
|
44
|
+
{subheading ? (
|
|
45
|
+
<p className="mt-4 text-[17px] leading-[1.6] text-pretty text-muted-foreground">
|
|
46
|
+
{subheading}
|
|
47
|
+
</p>
|
|
48
|
+
) : null}
|
|
49
|
+
{supportLink ? (
|
|
50
|
+
// Fixed copy in the renderer: the field stores only an href.
|
|
59
51
|
<Link
|
|
60
|
-
className=
|
|
52
|
+
className={cn(
|
|
53
|
+
buttonVariants({ variant: "link" }),
|
|
54
|
+
"mt-5 h-auto gap-1.5 p-0 text-[15px] font-medium text-primary no-underline hover:underline"
|
|
55
|
+
)}
|
|
61
56
|
href={supportLink}
|
|
62
57
|
>
|
|
63
|
-
|
|
58
|
+
Open an issue
|
|
59
|
+
<ArrowRight className="size-4" />
|
|
64
60
|
</Link>
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<Accordion className="border-t border-border" defaultValue={["faq-0"]}>
|
|
65
|
+
{faqs.map((faq, index) => (
|
|
66
|
+
<AccordionItem
|
|
67
|
+
className="border-b border-border"
|
|
68
|
+
key={`${faq.question}-${index}`}
|
|
69
|
+
value={`faq-${index}`}
|
|
70
|
+
>
|
|
71
|
+
<AccordionTrigger className="flex min-h-[52px] w-full items-start justify-between gap-6 py-[18px] text-left text-base font-medium text-foreground">
|
|
72
|
+
{faq.question}
|
|
73
|
+
</AccordionTrigger>
|
|
74
|
+
<AccordionContent className="max-w-[68ch] pt-3 pb-[18px] text-[15px] leading-[1.65] text-muted-foreground">
|
|
75
|
+
{faq.answer}
|
|
76
|
+
</AccordionContent>
|
|
77
|
+
</AccordionItem>
|
|
78
|
+
))}
|
|
79
|
+
</Accordion>
|
|
67
80
|
</div>
|
|
68
|
-
</
|
|
81
|
+
</Container>
|
|
69
82
|
</section>
|
|
70
83
|
)
|
|
71
84
|
}
|
|
@@ -1,73 +1,58 @@
|
|
|
1
1
|
import type { BlockComponentProps } from "@vexcms/react"
|
|
2
|
-
import type { ComponentPropsWithRef } from "react"
|
|
3
2
|
|
|
4
3
|
import { icons } from "lucide-react"
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import type { FeaturesBlock } from "~/vex.types"
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
import { Container } from "~/components/Container"
|
|
8
|
+
import { SectionHeader } from "~/components/SectionHeader"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const Icon = icons[name as keyof typeof icons]
|
|
12
|
-
if (!Icon) return null
|
|
13
|
-
return <Icon className="size-4" />
|
|
14
|
-
}
|
|
10
|
+
export { featuresBlock } from "./config"
|
|
15
11
|
|
|
16
|
-
function CardDecorator({ children, ...divProps }: ComponentPropsWithRef<"div">) {
|
|
17
|
-
return (
|
|
18
|
-
<div
|
|
19
|
-
className="relative mx-auto size-36 duration-200 [--color-border:color-mix(in_oklab,var(--color-zinc-950)10%,transparent)] group-hover:[--color-border:color-mix(in_oklab,var(--color-zinc-950)20%,transparent)] dark:[--color-border:color-mix(in_oklab,var(--color-white)15%,transparent)] dark:group-hover:bg-white/5 dark:group-hover:[--color-border:color-mix(in_oklab,var(--color-white)20%,transparent)]"
|
|
20
|
-
{...divProps}
|
|
21
|
-
>
|
|
22
|
-
<div
|
|
23
|
-
aria-hidden
|
|
24
|
-
className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-border)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-border)_1px,transparent_1px)] bg-[size:24px_24px]"
|
|
25
|
-
/>
|
|
26
|
-
<div
|
|
27
|
-
aria-hidden
|
|
28
|
-
className="to-background absolute inset-0 bg-radial from-transparent to-75%"
|
|
29
|
-
/>
|
|
30
|
-
<div className="bg-background absolute inset-0 m-auto flex size-12 items-center justify-center border-t border-l">
|
|
31
|
-
{children}
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Features — six cards, hairline-joined.
|
|
15
|
+
*
|
|
16
|
+
* The grid is a 1px mesh (`gap-px` over a `bg-border` wrapper) rather than
|
|
17
|
+
* gapped cards with shadows. Dark mode has no box-shadow at all, so gapped
|
|
18
|
+
* cards would lose their separation there; the mesh reads identically in both
|
|
19
|
+
* modes and suits the sharp radius.
|
|
20
|
+
*/
|
|
37
21
|
export default function FeaturesBlock({ block }: BlockComponentProps) {
|
|
38
|
-
const { heading, subheading
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
22
|
+
const { features, heading, subheading } = block as FeaturesBlock
|
|
23
|
+
const items = features ?? []
|
|
24
|
+
|
|
25
|
+
if (items.length === 0) {return null}
|
|
43
26
|
|
|
44
27
|
return (
|
|
45
|
-
<section className="
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
28
|
+
<section className="py-14 md:py-20 xl:py-28">
|
|
29
|
+
<Container>
|
|
30
|
+
<SectionHeader heading={heading} subheading={subheading} />
|
|
31
|
+
|
|
32
|
+
<div className="grid gap-px overflow-hidden rounded-md border border-border bg-border md:grid-cols-2 xl:grid-cols-3">
|
|
33
|
+
{items.map((feature, index) => {
|
|
34
|
+
const Glyph = feature.icon ? icons[feature.icon as keyof typeof icons] : undefined
|
|
35
|
+
return (
|
|
36
|
+
<div className="bg-background p-6" key={`${feature.title}-${index}`}>
|
|
37
|
+
{/* An unresolvable icon drops the frame entirely rather than
|
|
38
|
+
leaving an empty square; the title then starts the cell and
|
|
39
|
+
a mixed array still reads as intentional. */}
|
|
40
|
+
{Glyph ? (
|
|
41
|
+
<span className="mb-4 flex size-8 items-center justify-center rounded-sm border border-border">
|
|
42
|
+
<Glyph className="size-4 text-primary" />
|
|
43
|
+
</span>
|
|
44
|
+
) : null}
|
|
45
|
+
<h3 className="text-[17px] leading-[1.35] font-semibold tracking-[-0.01em] text-foreground xl:text-lg">
|
|
46
|
+
{feature.title}
|
|
47
|
+
</h3>
|
|
48
|
+
<p className="mt-2 text-sm leading-[1.6] text-pretty text-muted-foreground">
|
|
49
|
+
{feature.description}
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
})}
|
|
52
54
|
</div>
|
|
53
|
-
|
|
54
|
-
{(features ?? []).map((feature, index) => (
|
|
55
|
-
<div className="group shadow-zinc-950/5" key={index}>
|
|
56
|
-
<CardHeader className="pb-3">
|
|
57
|
-
{feature.icon && (
|
|
58
|
-
<CardDecorator>
|
|
59
|
-
<LucideIcon name={feature.icon} />
|
|
60
|
-
</CardDecorator>
|
|
61
|
-
)}
|
|
62
|
-
<h3 className="mt-6 font-medium">{feature.title}</h3>
|
|
63
|
-
</CardHeader>
|
|
64
|
-
<CardContent>
|
|
65
|
-
<p className="text-sm">{feature.description}</p>
|
|
66
|
-
</CardContent>
|
|
67
|
-
</div>
|
|
68
|
-
))}
|
|
69
|
-
</Card>
|
|
70
|
-
</div>
|
|
55
|
+
</Container>
|
|
71
56
|
</section>
|
|
72
57
|
)
|
|
73
58
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { array, defineBlock, group, text, upload } from "@vexcms/core"
|
|
2
2
|
|
|
3
3
|
import { TABLE_SLUG_IMAGES } from "~/db/constants"
|
|
4
|
+
|
|
4
5
|
import { BLOCK_SLUG_FOOTER } from "../constants"
|
|
5
6
|
|
|
6
7
|
export const footerBlock = defineBlock({
|
|
@@ -22,6 +23,7 @@ export const footerBlock = defineBlock({
|
|
|
22
23
|
links: array({
|
|
23
24
|
label: "Links",
|
|
24
25
|
items: group({
|
|
26
|
+
label: "Link",
|
|
25
27
|
fields: {
|
|
26
28
|
label: text({ label: "Label", required: true }),
|
|
27
29
|
href: text({ label: "Link", required: true }),
|
|
@@ -31,8 +33,8 @@ export const footerBlock = defineBlock({
|
|
|
31
33
|
{ label: "Features", href: "/features" },
|
|
32
34
|
{ label: "Pricing", href: "/pricing" },
|
|
33
35
|
{ label: "Roadmap", href: "/roadmap" },
|
|
34
|
-
{ label: "Documentation", href: "
|
|
35
|
-
{ label: "GitHub", href: "https://github.com/
|
|
36
|
+
{ label: "Documentation", href: "https://docs.vexcms.dev" },
|
|
37
|
+
{ label: "GitHub", href: "https://github.com/ianyimi/vex" },
|
|
36
38
|
{ label: "npm", href: "https://www.npmjs.com/package/@vexcms/core" },
|
|
37
39
|
{ label: "Convex", href: "https://convex.dev" },
|
|
38
40
|
],
|
|
@@ -40,6 +42,7 @@ export const footerBlock = defineBlock({
|
|
|
40
42
|
socialLinks: array({
|
|
41
43
|
label: "Social Links",
|
|
42
44
|
items: group({
|
|
45
|
+
label: "Social Link",
|
|
43
46
|
fields: {
|
|
44
47
|
platform: text({ label: "Platform", required: true }),
|
|
45
48
|
href: text({ label: "URL", required: true }),
|
|
@@ -50,7 +53,7 @@ export const footerBlock = defineBlock({
|
|
|
50
53
|
},
|
|
51
54
|
}),
|
|
52
55
|
defaultValue: [
|
|
53
|
-
{ platform: "GitHub", href: "https://github.com/
|
|
56
|
+
{ platform: "GitHub", href: "https://github.com/ianyimi/vex", icon: "Github" },
|
|
54
57
|
{ platform: "X", href: "https://x.com/vexcms", icon: "Twitter" },
|
|
55
58
|
],
|
|
56
59
|
}),
|
|
@@ -1,71 +1,108 @@
|
|
|
1
1
|
import type { BlockComponentProps } from "@vexcms/react"
|
|
2
2
|
|
|
3
|
+
import { buttonVariants, cn } from "@vexcms/react"
|
|
3
4
|
import { icons } from "lucide-react"
|
|
4
5
|
import Link from "next/link"
|
|
5
6
|
|
|
7
|
+
import type { FooterBlock } from "~/vex.types"
|
|
8
|
+
|
|
9
|
+
import { BrandMark } from "~/components/BrandMark"
|
|
10
|
+
import { Container } from "~/components/Container"
|
|
11
|
+
import { MediaImage } from "~/components/MediaImage"
|
|
12
|
+
|
|
6
13
|
export { footerBlock } from "./config"
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Site footer — one rule, one row of links, one line of law.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately not a four-column sitemap: `links` is a flat array with no
|
|
19
|
+
* group field, so column headings would be content the CMS cannot store. A
|
|
20
|
+
* single wrapping row is the shape the data actually has.
|
|
21
|
+
*/
|
|
22
|
+
export default function FooterBlockRenderer({ block }: BlockComponentProps) {
|
|
23
|
+
const { copyright, links, logoImage, logoText, socialLinks } = block as FooterBlock
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
const
|
|
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
|
-
}
|
|
25
|
+
const items = links ?? []
|
|
26
|
+
const socials = socialLinks ?? []
|
|
22
27
|
|
|
23
28
|
return (
|
|
24
|
-
<footer>
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
<footer className="py-12 md:py-14">
|
|
30
|
+
<Container>
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
"flex flex-col items-start justify-between gap-8 md:flex-row md:gap-12",
|
|
34
|
+
// The rule only exists to separate the links from the legal line.
|
|
35
|
+
items.length > 0 && "border-b border-border pb-8"
|
|
36
|
+
)}
|
|
30
37
|
>
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
<div className="flex shrink-0 items-center gap-2 text-foreground">
|
|
39
|
+
{logoImage?.length ? (
|
|
40
|
+
<MediaImage alt={logoText ?? ""} className="h-5 w-auto" height={20} value={logoImage} width={96} />
|
|
41
|
+
) : (
|
|
42
|
+
<>
|
|
43
|
+
<BrandMark className="size-4 text-primary" />
|
|
44
|
+
{logoText ? (
|
|
45
|
+
<span className="text-[15px] font-extrabold tracking-[-0.03em]">{logoText}</span>
|
|
46
|
+
) : null}
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
{items.length > 0 ? (
|
|
52
|
+
<ul className="grid w-full grid-cols-2 gap-x-7 gap-y-2 md:flex md:w-auto md:max-w-[640px] md:flex-wrap md:justify-end">
|
|
53
|
+
{items.map((link) => (
|
|
54
|
+
<li key={`${link.label}-${link.href}`}>
|
|
55
|
+
<Link
|
|
56
|
+
className="text-sm text-muted-foreground transition-colors duration-[180ms] ease-[var(--ease-emphasized)] hover:text-foreground"
|
|
57
|
+
href={link.href}
|
|
58
|
+
>
|
|
59
|
+
{link.label}
|
|
60
|
+
</Link>
|
|
61
|
+
</li>
|
|
62
|
+
))}
|
|
63
|
+
</ul>
|
|
64
|
+
) : null}
|
|
44
65
|
</div>
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
)}
|
|
67
|
+
<div className="flex flex-col items-start justify-between gap-5 pt-5 md:flex-row md:items-center">
|
|
68
|
+
{copyright ? (
|
|
69
|
+
<p className="text-[13px] text-muted-foreground">
|
|
70
|
+
© {new Date().getFullYear()} {copyright}
|
|
71
|
+
</p>
|
|
72
|
+
) : null}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
{socials.length > 0 ? (
|
|
75
|
+
<ul className="flex items-center gap-2">
|
|
76
|
+
{socials.map((social) => {
|
|
77
|
+
// An unresolvable lucide name must not render an empty square —
|
|
78
|
+
// fall back to the platform name as a text link.
|
|
79
|
+
const Glyph = social.icon ? icons[social.icon as keyof typeof icons] : undefined
|
|
80
|
+
return (
|
|
81
|
+
<li key={`${social.platform}-${social.href}`}>
|
|
82
|
+
<Link
|
|
83
|
+
aria-label={social.platform}
|
|
84
|
+
className={cn(
|
|
85
|
+
Glyph
|
|
86
|
+
? cn(
|
|
87
|
+
buttonVariants({ size: "icon-sm", variant: "ghost" }),
|
|
88
|
+
"border border-border"
|
|
89
|
+
)
|
|
90
|
+
: "text-[13px] text-muted-foreground hover:text-foreground",
|
|
91
|
+
"transition-colors duration-[180ms] ease-[var(--ease-emphasized)] active:translate-y-px"
|
|
92
|
+
)}
|
|
93
|
+
href={social.href}
|
|
94
|
+
rel="noreferrer"
|
|
95
|
+
target="_blank"
|
|
96
|
+
>
|
|
97
|
+
{Glyph ? <Glyph className="size-4" /> : social.platform}
|
|
98
|
+
</Link>
|
|
99
|
+
</li>
|
|
100
|
+
)
|
|
101
|
+
})}
|
|
102
|
+
</ul>
|
|
103
|
+
) : null}
|
|
104
|
+
</div>
|
|
105
|
+
</Container>
|
|
69
106
|
</footer>
|
|
70
107
|
)
|
|
71
108
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { array, defineBlock, group, select, text, upload } from "@vexcms/core"
|
|
2
2
|
|
|
3
3
|
import { TABLE_SLUG_IMAGES } from "~/db/constants"
|
|
4
|
+
|
|
4
5
|
import { BLOCK_SLUG_HEADER } from "../constants"
|
|
5
6
|
|
|
6
7
|
export const headerBlock = defineBlock({
|
|
@@ -24,6 +25,7 @@ export const headerBlock = defineBlock({
|
|
|
24
25
|
menuItems: array({
|
|
25
26
|
label: "Menu Items",
|
|
26
27
|
items: group({
|
|
28
|
+
label: "Menu Item",
|
|
27
29
|
fields: {
|
|
28
30
|
label: text({ label: "Label", required: true }),
|
|
29
31
|
href: text({ label: "Link", required: true }),
|
|
@@ -33,12 +35,13 @@ export const headerBlock = defineBlock({
|
|
|
33
35
|
{ label: "Features", href: "/features" },
|
|
34
36
|
{ label: "Pricing", href: "/pricing" },
|
|
35
37
|
{ label: "Roadmap", href: "/roadmap" },
|
|
36
|
-
{ label: "Docs", href: "
|
|
38
|
+
{ label: "Docs", href: "https://docs.vexcms.dev" },
|
|
37
39
|
],
|
|
38
40
|
}),
|
|
39
41
|
actionButtons: array({
|
|
40
42
|
label: "Action Buttons",
|
|
41
43
|
items: group({
|
|
44
|
+
label: "Action Button",
|
|
42
45
|
fields: {
|
|
43
46
|
label: text({ label: "Button Label", required: true }),
|
|
44
47
|
href: text({ label: "Button Link", required: true }),
|
|
@@ -54,8 +57,12 @@ export const headerBlock = defineBlock({
|
|
|
54
57
|
},
|
|
55
58
|
}),
|
|
56
59
|
defaultValue: [
|
|
57
|
-
{ label: "GitHub", href: "https://github.com/
|
|
58
|
-
{
|
|
60
|
+
{ label: "GitHub", href: "https://github.com/ianyimi/vex", variant: "ghost" },
|
|
61
|
+
{
|
|
62
|
+
label: "Get Started",
|
|
63
|
+
href: "https://docs.vexcms.dev/guides/quickstart/",
|
|
64
|
+
variant: "default",
|
|
65
|
+
},
|
|
59
66
|
],
|
|
60
67
|
}),
|
|
61
68
|
},
|