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.
Files changed (59) 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 +8 -8
  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/motion-primitives/animated-group.tsx +138 -0
  28. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +292 -0
  29. package/templates/marketing-site/src/lib/metadata.ts +104 -0
  30. package/templates/marketing-site/src/vexcms/access.ts +28 -0
  31. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +36 -0
  32. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +46 -0
  33. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +70 -0
  34. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +71 -0
  35. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +61 -0
  36. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +73 -0
  37. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +65 -0
  38. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +71 -0
  39. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +65 -0
  40. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +174 -0
  41. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +52 -0
  42. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +152 -0
  43. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +68 -0
  44. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +74 -0
  45. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +154 -0
  46. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +122 -0
  47. package/templates/marketing-site/src/vexcms/blocks/config.ts +40 -0
  48. package/templates/marketing-site/src/vexcms/blocks/constants.ts +9 -0
  49. package/templates/marketing-site/src/vexcms/blocks/index.ts +39 -0
  50. package/templates/marketing-site/src/vexcms/collections/footers.ts +11 -8
  51. package/templates/marketing-site/src/vexcms/collections/headers.ts +10 -14
  52. package/templates/marketing-site/src/vexcms/collections/index.ts +0 -1
  53. package/templates/marketing-site/src/vexcms/collections/pages.ts +30 -4
  54. package/templates/marketing-site/src/vexcms/collections/themes.ts +107 -14
  55. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -0
  56. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +57 -0
  57. package/templates/marketing-site/vex.config.ts +5 -5
  58. package/templates/base-nextjs/src/auth/permissions.ts +0 -102
  59. package/templates/marketing-site/src/vexcms/collections/site_settings.ts +0 -31
@@ -0,0 +1,70 @@
1
+ import { array, defineBlock, object, text } from "@vexcms/core"
2
+
3
+ import { BLOCK_SLUG_FAQ } from "../constants"
4
+
5
+ export const faqBlock = defineBlock({
6
+ slug: BLOCK_SLUG_FAQ,
7
+ label: "FAQ",
8
+ fields: {
9
+ heading: text({
10
+ label: "Heading",
11
+ required: true,
12
+ defaultValue: "Frequently Asked Questions",
13
+ }),
14
+ subheading: text({
15
+ label: "Subheading",
16
+ defaultValue:
17
+ "Everything you need to know about Vex CMS and building with Convex.",
18
+ }),
19
+ supportLink: text({
20
+ label: "Support Link",
21
+ defaultValue: "https://github.com/vexcms/vex/issues",
22
+ admin: { description: "URL to contact support page" },
23
+ }),
24
+ items: array({
25
+ label: "FAQ Items",
26
+ required: true,
27
+ items: object({
28
+ fields: {
29
+ question: text({ label: "Question", required: true }),
30
+ answer: text({ label: "Answer", required: true }),
31
+ },
32
+ }),
33
+ defaultValue: [
34
+ {
35
+ question: "What is Vex CMS?",
36
+ answer:
37
+ "Vex CMS is a headless content management system built on Convex. It provides real-time data, type-safe schemas, draft/publish workflows, live preview, and a beautiful admin panel — all configured with TypeScript.",
38
+ },
39
+ {
40
+ question: "How is Vex different from other headless CMS platforms?",
41
+ answer:
42
+ "Unlike traditional headless CMS platforms that use REST or GraphQL APIs, Vex is powered by Convex's real-time database. This means content updates are instant across all clients, with no polling or webhooks needed. Your schema is defined in code, giving you full type safety from database to frontend.",
43
+ },
44
+ {
45
+ question: "Do I need to know Convex to use Vex CMS?",
46
+ answer:
47
+ "Basic familiarity with Convex helps, but Vex handles most of the complexity for you. The CLI generates your Convex schema, queries, and types automatically from your collection definitions. You just define your fields and Vex does the rest.",
48
+ },
49
+ {
50
+ question: "Can I use Vex with any frontend framework?",
51
+ answer:
52
+ "Vex CMS is framework-agnostic at the data layer — any app that can use Convex can use Vex. The admin panel and UI components are built for Next.js, but the core package and generated queries work with any Convex-compatible frontend.",
53
+ },
54
+ {
55
+ question: "Is Vex CMS free?",
56
+ answer:
57
+ "Yes, Vex CMS is open source and free to use. You only pay for your Convex usage, which has a generous free tier for most projects.",
58
+ },
59
+ {
60
+ question: "How do I get started?",
61
+ answer:
62
+ "Run `npx create-vexcms@latest` to scaffold a new project with Vex CMS pre-configured. The CLI sets up your Next.js app, Convex backend, authentication, and admin panel in under a minute.",
63
+ },
64
+ ],
65
+ }),
66
+ },
67
+ admin: {
68
+ icon: "help-circle",
69
+ },
70
+ })
@@ -0,0 +1,71 @@
1
+ import type { BlockComponentProps } from "@vexcms/ui"
2
+
3
+ import Link from "next/link"
4
+
5
+ import {
6
+ Accordion,
7
+ AccordionContent,
8
+ AccordionItem,
9
+ AccordionTrigger,
10
+ } from "~/components/ui/accordion"
11
+
12
+ export { faqBlock } from "./config"
13
+
14
+ export default function FAQBlock({ block }: BlockComponentProps) {
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
+ }
21
+
22
+ return (
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>
35
+
36
+ <div className="mx-auto mt-12 max-w-xl">
37
+ <Accordion className="bg-muted dark:bg-muted/50 w-full rounded-2xl p-1">
38
+ {(items ?? []).map((faq, index) => (
39
+ <div className="group" key={index}>
40
+ <AccordionItem
41
+ className="data-open:bg-card dark:data-open:bg-muted rounded-xl border-none px-7 py-1 data-open:border-none data-open:shadow-sm"
42
+ value={`${index}`}
43
+ >
44
+ <AccordionTrigger className="cursor-pointer text-base hover:no-underline">
45
+ {faq.question}
46
+ </AccordionTrigger>
47
+ <AccordionContent>
48
+ <p className="text-base">{faq.answer}</p>
49
+ </AccordionContent>
50
+ </AccordionItem>
51
+ <hr className="mx-7 border-dashed group-last:hidden" />
52
+ </div>
53
+ ))}
54
+ </Accordion>
55
+
56
+ {supportLink && (
57
+ <p className="text-muted-foreground mt-6 px-8">
58
+ {"Can't find what you're looking for? Contact our "}
59
+ <Link
60
+ className="text-primary font-medium hover:underline"
61
+ href={supportLink}
62
+ >
63
+ support team
64
+ </Link>
65
+ </p>
66
+ )}
67
+ </div>
68
+ </div>
69
+ </section>
70
+ )
71
+ }
@@ -0,0 +1,61 @@
1
+ import { array, defineBlock, object, text } from "@vexcms/core"
2
+
3
+ import IconPickerField from "~/components/admin/IconPickerField"
4
+ import { BLOCK_SLUG_FEATURES } from "../constants"
5
+
6
+ export const featuresBlock = defineBlock({
7
+ slug: BLOCK_SLUG_FEATURES,
8
+ label: "Features",
9
+ fields: {
10
+ heading: text({
11
+ label: "Heading",
12
+ required: true,
13
+ defaultValue: "Everything you need to manage content",
14
+ }),
15
+ subheading: text({
16
+ label: "Subheading",
17
+ defaultValue:
18
+ "Built on Convex's real-time infrastructure with a developer experience that doesn't compromise on power.",
19
+ }),
20
+ features: array({
21
+ label: "Features",
22
+ required: true,
23
+ items: object({
24
+ fields: {
25
+ title: text({ label: "Title", required: true }),
26
+ description: text({ label: "Description", required: true }),
27
+ icon: text({
28
+ label: "Icon",
29
+ admin: {
30
+ description: "Lucide icon name",
31
+ components: { Field: IconPickerField },
32
+ },
33
+ }),
34
+ },
35
+ }),
36
+ defaultValue: [
37
+ {
38
+ title: "Real-Time by Default",
39
+ description:
40
+ "Every query is live. Content updates appear instantly across all connected clients — no polling, no webhooks.",
41
+ icon: "Zap",
42
+ },
43
+ {
44
+ title: "Type-Safe Schemas",
45
+ description:
46
+ "Define your collections with TypeScript. Vex generates Convex schemas, Zod validators, and typed queries automatically.",
47
+ icon: "Shield",
48
+ },
49
+ {
50
+ title: "Developer First",
51
+ description:
52
+ "Code-first configuration, CLI tooling, and a clean API. Build with the tools you already know and love.",
53
+ icon: "Code",
54
+ },
55
+ ],
56
+ }),
57
+ },
58
+ admin: {
59
+ icon: "layout-grid",
60
+ },
61
+ })
@@ -0,0 +1,73 @@
1
+ import type { BlockComponentProps } from "@vexcms/ui"
2
+ import type { ComponentPropsWithRef } from "react"
3
+
4
+ import { icons } from "lucide-react"
5
+
6
+ import { Card, CardContent, CardHeader } from "~/components/ui/card"
7
+
8
+ export { featuresBlock } from "./config"
9
+
10
+ function LucideIcon({ name }: { name: string }) {
11
+ const Icon = icons[name as keyof typeof icons]
12
+ if (!Icon) return null
13
+ return <Icon className="size-4" />
14
+ }
15
+
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
+
37
+ export default function FeaturesBlock({ block }: BlockComponentProps) {
38
+ const { heading, subheading, features } = block as unknown as {
39
+ heading: string
40
+ subheading?: string
41
+ features?: Array<{ title: string; description: string; icon?: string }>
42
+ }
43
+
44
+ return (
45
+ <section className="bg-zinc-50 py-16 md:py-32 dark:bg-transparent">
46
+ <div className="@container mx-auto max-w-5xl px-6">
47
+ <div className="text-center">
48
+ <h2 className="text-4xl font-semibold text-balance lg:text-5xl">
49
+ {heading}
50
+ </h2>
51
+ {subheading && <p className="mt-4">{subheading}</p>}
52
+ </div>
53
+ <Card className="mx-auto mt-8 grid max-w-sm divide-y overflow-hidden shadow-zinc-950/5 *:text-center md:mt-16 @min-4xl:max-w-full @min-4xl:grid-cols-3 @min-4xl:divide-x @min-4xl:divide-y-0">
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>
71
+ </section>
72
+ )
73
+ }
@@ -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
+ }