Template
Add theme toggle, hero snippet, and reorganize homepage components
- Set up next-themes with a light/dark toggle in the header and move all sections onto theme tokens; dark mode stays pure black - Replace the hero terminal line with the AI Elements Snippet component - Restructure components into layout/, sections/, and shared/ with site-wide config in lib/site.ts Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
a40edbffa9
commit
07a99b441c
+1
-1
@@ -118,7 +118,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0 0 0);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.205 0 0);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
|||||||
+14
-5
@@ -1,10 +1,12 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { ThemeProvider } from '@/components/layout/theme-provider';
|
||||||
|
import { siteConfig } from '@/lib/site';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Vertex — Build. Preview. Ship.',
|
title: siteConfig.title,
|
||||||
description:
|
description: siteConfig.description,
|
||||||
'Vertex gives frontend teams the tooling and cloud infrastructure to build, preview, and ship a faster web.',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -13,9 +15,16 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="dark h-full antialiased">
|
<html lang="en" suppressHydrationWarning className="h-full antialiased">
|
||||||
<body className="min-h-full flex flex-col bg-black text-white font-sans selection:bg-white selection:text-black">
|
<body className="min-h-full flex flex-col bg-background text-foreground font-sans selection:bg-foreground selection:text-background">
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="dark"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+15
-15
@@ -1,23 +1,23 @@
|
|||||||
import { Cta } from '@/components/site/cta';
|
import { SiteFooter } from '@/components/layout/site-footer';
|
||||||
import { Features } from '@/components/site/features';
|
import { SiteHeader } from '@/components/layout/site-header';
|
||||||
import { Footer } from '@/components/site/footer';
|
import { CtaSection } from '@/components/sections/cta-section';
|
||||||
import { Hero } from '@/components/site/hero';
|
import { FeaturesSection } from '@/components/sections/features-section';
|
||||||
import { LogoCloud } from '@/components/site/logo-cloud';
|
import { HeroSection } from '@/components/sections/hero-section';
|
||||||
import { Navbar } from '@/components/site/navbar';
|
import { LogosSection } from '@/components/sections/logos-section';
|
||||||
import { Stats } from '@/components/site/stats';
|
import { StatsSection } from '@/components/sections/stats-section';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<SiteHeader />
|
||||||
<main>
|
<main id="top">
|
||||||
<Hero />
|
<HeroSection />
|
||||||
<LogoCloud />
|
<LogosSection />
|
||||||
<Features />
|
<FeaturesSection />
|
||||||
<Stats />
|
<StatsSection />
|
||||||
<Cta />
|
<CtaSection />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<SiteFooter />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -22,6 +22,7 @@
|
|||||||
"menuColor": "default",
|
"menuColor": "default",
|
||||||
"menuAccent": "subtle",
|
"menuAccent": "subtle",
|
||||||
"registries": {
|
"registries": {
|
||||||
"@magicui": "https://magicui.design/r/{name}"
|
"@magicui": "https://magicui.design/r/{name}",
|
||||||
|
"@ai-elements": "https://ai-sdk.dev/elements/api/registry/{name}.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupButton,
|
||||||
|
InputGroupInput,
|
||||||
|
InputGroupText,
|
||||||
|
} from "@/components/ui/input-group";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
interface SnippetContextType {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SnippetContext = createContext<SnippetContextType>({
|
||||||
|
code: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SnippetProps = ComponentProps<typeof InputGroup> & {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Snippet = ({
|
||||||
|
code,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SnippetProps) => {
|
||||||
|
const contextValue = useMemo(() => ({ code }), [code]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SnippetContext.Provider value={contextValue}>
|
||||||
|
<InputGroup className={cn("font-mono", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</InputGroup>
|
||||||
|
</SnippetContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SnippetAddonProps = ComponentProps<typeof InputGroupAddon>;
|
||||||
|
|
||||||
|
export const SnippetAddon = (props: SnippetAddonProps) => (
|
||||||
|
<InputGroupAddon {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SnippetTextProps = ComponentProps<typeof InputGroupText>;
|
||||||
|
|
||||||
|
export const SnippetText = ({ className, ...props }: SnippetTextProps) => (
|
||||||
|
<InputGroupText
|
||||||
|
className={cn("pl-2 font-normal text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SnippetInputProps = Omit<
|
||||||
|
ComponentProps<typeof InputGroupInput>,
|
||||||
|
"readOnly" | "value"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const SnippetInput = ({ className, ...props }: SnippetInputProps) => {
|
||||||
|
const { code } = useContext(SnippetContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroupInput
|
||||||
|
className={cn("text-foreground", className)}
|
||||||
|
readOnly
|
||||||
|
value={code}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SnippetCopyButtonProps = ComponentProps<typeof InputGroupButton> & {
|
||||||
|
onCopy?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SnippetCopyButton = ({
|
||||||
|
onCopy,
|
||||||
|
onError,
|
||||||
|
timeout = 2000,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: SnippetCopyButtonProps) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const timeoutRef = useRef<number>(0);
|
||||||
|
const { code } = useContext(SnippetContext);
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(async () => {
|
||||||
|
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
||||||
|
onError?.(new Error("Clipboard API not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isCopied) {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
setIsCopied(true);
|
||||||
|
onCopy?.();
|
||||||
|
timeoutRef.current = window.setTimeout(
|
||||||
|
() => setIsCopied(false),
|
||||||
|
timeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}, [code, onCopy, onError, timeout, isCopied]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
window.clearTimeout(timeoutRef.current);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroupButton
|
||||||
|
aria-label="Copy"
|
||||||
|
className={className}
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
size="icon-sm"
|
||||||
|
title="Copy"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <Icon className="size-3.5" size={14} />}
|
||||||
|
</InputGroupButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { RiCloseLine, RiMenuLine } from '@remixicon/react';
|
import { RiCloseLine, RiMenuLine } from '@remixicon/react';
|
||||||
|
|
||||||
import { Logo } from '@/components/site/logo';
|
import { Brand } from '@/components/shared/brand';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
@@ -12,12 +12,7 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
|
import { siteConfig, type NavigationItem } from '@/lib/site';
|
||||||
export type NavigationItem = {
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function MobileNavigation({ items }: { items: NavigationItem[] }) {
|
export function MobileNavigation({ items }: { items: NavigationItem[] }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -39,7 +34,7 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Open navigation menu"
|
aria-label="Open navigation menu"
|
||||||
className="inline-flex size-10 items-center justify-center rounded-md text-white/70 transition-colors hover:bg-white/5 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 md:hidden"
|
className="inline-flex size-10 items-center justify-center rounded-md text-foreground/70 transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/30 md:hidden"
|
||||||
>
|
>
|
||||||
<RiMenuLine className="size-5" aria-hidden="true" />
|
<RiMenuLine className="size-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -48,8 +43,8 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
|
|||||||
<SheetContent
|
<SheetContent
|
||||||
side="top"
|
side="top"
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
overlayClassName="z-[60] bg-black/60 duration-300 supports-backdrop-filter:backdrop-blur-md md:hidden"
|
overlayClassName="z-[60] bg-black/45 duration-300 supports-backdrop-filter:backdrop-blur-md md:hidden"
|
||||||
className="z-[61] max-h-dvh w-full gap-0 overflow-hidden rounded-none border-white/10 bg-black text-white outline-none duration-300 data-[side=top]:data-open:slide-in-from-top data-[side=top]:data-closed:slide-out-to-top md:hidden"
|
className="z-[61] max-h-dvh w-full gap-0 overflow-hidden rounded-none border-border bg-background text-foreground outline-none duration-300 data-[side=top]:data-open:slide-in-from-top data-[side=top]:data-closed:slide-out-to-top md:hidden"
|
||||||
onCloseAutoFocus={(event) => {
|
onCloseAutoFocus={(event) => {
|
||||||
// Let the anchor move focus to its section without the trigger
|
// Let the anchor move focus to its section without the trigger
|
||||||
// interrupting the page's smooth scroll when the sheet closes.
|
// interrupting the page's smooth scroll when the sheet closes.
|
||||||
@@ -57,13 +52,13 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
|
|||||||
navigating.current = false;
|
navigating.current = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex h-16 shrink-0 items-center justify-between border-b border-white/10 px-6">
|
<div className="flex h-16 shrink-0 items-center justify-between border-b border-border px-6">
|
||||||
<Logo />
|
<Brand />
|
||||||
<SheetClose asChild>
|
<SheetClose asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Close navigation menu"
|
aria-label="Close navigation menu"
|
||||||
className="inline-flex size-10 items-center justify-center rounded-md text-white/60 transition-colors hover:bg-white/5 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
|
className="inline-flex size-10 items-center justify-center rounded-md text-foreground/60 transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/30"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="size-5" aria-hidden="true" />
|
<RiCloseLine className="size-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -71,8 +66,8 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 overflow-y-auto overscroll-contain p-3">
|
<div className="min-h-0 overflow-y-auto overscroll-contain p-3">
|
||||||
<SheetTitle className="px-3 pb-2 pt-3 text-[10px] font-medium uppercase tracking-[0.2em] text-white/40">
|
<SheetTitle className="px-3 pb-2 pt-3 text-[10px] font-medium uppercase tracking-[0.2em] text-foreground/40">
|
||||||
Explore Vertex
|
Explore {siteConfig.name}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription className="sr-only">
|
<SheetDescription className="sr-only">
|
||||||
Explore our products, solutions, resources, documentation, and pricing.
|
Explore our products, solutions, resources, documentation, and pricing.
|
||||||
@@ -87,13 +82,13 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigating.current = true;
|
navigating.current = true;
|
||||||
}}
|
}}
|
||||||
className="flex items-center rounded-xl px-3 py-3 transition-colors hover:bg-white/5 focus-visible:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/30"
|
className="flex items-center rounded-xl px-3 py-3 transition-colors hover:bg-foreground/5 focus-visible:bg-foreground/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-foreground/30"
|
||||||
>
|
>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
<span className="block text-xl font-medium tracking-tight">
|
<span className="block text-xl font-medium tracking-tight">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-0.5 block text-xs leading-5 text-white/45">
|
<span className="mt-0.5 block text-xs leading-5 text-foreground/45">
|
||||||
{description}
|
{description}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -105,25 +100,25 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0 space-y-2 border-t border-white/10 bg-white/[0.02] p-5">
|
<div className="shrink-0 space-y-2 border-t border-border bg-foreground/[0.02] p-5">
|
||||||
<SheetClose asChild>
|
<SheetClose asChild>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href={siteConfig.links.signup}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigating.current = true;
|
navigating.current = true;
|
||||||
}}
|
}}
|
||||||
className="flex h-12 items-center justify-center rounded-full bg-white px-4 text-sm font-medium text-black transition-colors hover:bg-white/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 focus-visible:ring-offset-2 focus-visible:ring-offset-black"
|
className="flex h-12 items-center justify-center rounded-full bg-foreground px-4 text-sm font-medium text-background transition-colors hover:bg-foreground/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
||||||
>
|
>
|
||||||
Sign Up
|
Sign Up
|
||||||
</a>
|
</a>
|
||||||
</SheetClose>
|
</SheetClose>
|
||||||
<SheetClose asChild>
|
<SheetClose asChild>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href={siteConfig.links.login}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigating.current = true;
|
navigating.current = true;
|
||||||
}}
|
}}
|
||||||
className="flex h-12 items-center justify-center rounded-full border border-white/10 px-4 text-sm font-medium text-white/70 transition-colors hover:bg-white/5 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
|
className="flex h-12 items-center justify-center rounded-full border border-border px-4 text-sm font-medium text-foreground/70 transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/30"
|
||||||
>
|
>
|
||||||
Log In
|
Log In
|
||||||
</a>
|
</a>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { RiGithubFill, RiTwitterXFill } from '@remixicon/react';
|
||||||
|
|
||||||
|
import { Brand } from '@/components/shared/brand';
|
||||||
|
import { Container } from '@/components/shared/container';
|
||||||
|
import { footerColumns, siteConfig } from '@/lib/site';
|
||||||
|
|
||||||
|
const socialLinks = [
|
||||||
|
{ label: 'GitHub', href: siteConfig.links.github, Icon: RiGithubFill },
|
||||||
|
{ label: 'X (Twitter)', href: siteConfig.links.twitter, Icon: RiTwitterXFill },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SiteFooter() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-border">
|
||||||
|
<Container className="py-16">
|
||||||
|
<div className="grid gap-10 md:grid-cols-[1.5fr_repeat(4,1fr)]">
|
||||||
|
<div>
|
||||||
|
<Brand />
|
||||||
|
<p className="mt-4 max-w-xs text-sm text-foreground/45">
|
||||||
|
{siteConfig.tagline}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{footerColumns.map((column) => (
|
||||||
|
<div key={column.heading}>
|
||||||
|
<h4 className="text-sm font-medium text-foreground/80">
|
||||||
|
{column.heading}
|
||||||
|
</h4>
|
||||||
|
<ul className="mt-4 space-y-3">
|
||||||
|
{column.links.map((link) => (
|
||||||
|
<li key={link.label}>
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-foreground/45 transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-14 flex flex-col items-start justify-between gap-4 border-t border-border pt-8 text-sm text-foreground/40 sm:flex-row sm:items-center">
|
||||||
|
<span>
|
||||||
|
© {new Date().getFullYear()} {siteConfig.name}, Inc. All rights reserved.
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{socialLinks.map(({ label, href, Icon }) => (
|
||||||
|
<a
|
||||||
|
key={label}
|
||||||
|
href={href}
|
||||||
|
aria-label={label}
|
||||||
|
className="inline-flex size-8 items-center justify-center rounded-md text-foreground/45 transition-colors hover:bg-foreground/5 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Icon className="size-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { MobileNavigation } from '@/components/layout/mobile-navigation';
|
||||||
|
import { ThemeToggle } from '@/components/layout/theme-toggle';
|
||||||
|
import { Brand } from '@/components/shared/brand';
|
||||||
|
import { Container } from '@/components/shared/container';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { navigationItems, siteConfig } from '@/lib/site';
|
||||||
|
|
||||||
|
export function SiteHeader() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 border-b border-border bg-background/70 backdrop-blur-xl">
|
||||||
|
<Container className="flex h-16 items-center justify-between">
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<a href="#top" aria-label={`${siteConfig.name} home`}>
|
||||||
|
<Brand />
|
||||||
|
</a>
|
||||||
|
<nav aria-label="Main navigation" className="hidden md:block">
|
||||||
|
<ul className="flex items-center gap-6">
|
||||||
|
{navigationItems.map(({ label, href }) => (
|
||||||
|
<li key={label}>
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className="text-sm text-foreground/60 transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
variant="ghost"
|
||||||
|
className="hidden text-foreground/70 hover:bg-foreground/5 hover:text-foreground sm:inline-flex"
|
||||||
|
>
|
||||||
|
<a href={siteConfig.links.login}>Log In</a>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
className="hidden rounded-full bg-foreground text-background hover:bg-foreground/90 sm:inline-flex"
|
||||||
|
>
|
||||||
|
<a href={siteConfig.links.signup}>Sign Up</a>
|
||||||
|
</Button>
|
||||||
|
<MobileNavigation items={navigationItems} />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useSyncExternalStore } from 'react';
|
||||||
|
import { RiMoonLine, RiSunLine } from '@remixicon/react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
const emptySubscribe = () => () => {};
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const mounted = useSyncExternalStore(
|
||||||
|
emptySubscribe,
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
|
const { resolvedTheme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
const isDark = mounted && resolvedTheme === 'dark';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-lg"
|
||||||
|
aria-label={
|
||||||
|
mounted ? `Switch to ${isDark ? 'light' : 'dark'} theme` : 'Toggle theme'
|
||||||
|
}
|
||||||
|
className="text-foreground/70 hover:bg-foreground/5 hover:text-foreground"
|
||||||
|
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
||||||
|
>
|
||||||
|
{mounted ? (
|
||||||
|
isDark ? (
|
||||||
|
<RiSunLine className="size-4" />
|
||||||
|
) : (
|
||||||
|
<RiMoonLine className="size-4" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="size-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ActionButtons } from '@/components/shared/action-buttons';
|
||||||
|
import { Container } from '@/components/shared/container';
|
||||||
|
import { GradientGlow } from '@/components/shared/gradient-glow';
|
||||||
|
import { SectionHeading } from '@/components/shared/section-heading';
|
||||||
|
|
||||||
|
export function CtaSection() {
|
||||||
|
return (
|
||||||
|
<section className="relative overflow-hidden">
|
||||||
|
<GradientGlow className="left-1/2 top-1/2 h-[300px] w-[600px] -translate-x-1/2 -translate-y-1/2 opacity-25" />
|
||||||
|
<Container className="relative py-28">
|
||||||
|
<SectionHeading
|
||||||
|
align="center"
|
||||||
|
title="Ship your first project in minutes."
|
||||||
|
description="Import a repository or start from a template. No credit card required to deploy your first preview."
|
||||||
|
className="[&>h2]:text-4xl sm:[&>h2]:text-5xl"
|
||||||
|
/>
|
||||||
|
<ActionButtons
|
||||||
|
className="mt-9"
|
||||||
|
primary={{ label: 'Start Deploying', href: '#' }}
|
||||||
|
secondary={{ label: 'Talk to Sales', href: '#' }}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { Container } from '@/components/shared/container';
|
||||||
|
import { SectionHeading } from '@/components/shared/section-heading';
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
step: '01',
|
||||||
|
title: 'Build',
|
||||||
|
description:
|
||||||
|
'Native support for 35+ frameworks. Push your code and Vertex handles the build pipeline, caching, and dependencies automatically.',
|
||||||
|
icon: <path d="M8 6L2 12l6 6M16 6l6 6-6 6" strokeWidth="1.5" fill="none" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '02',
|
||||||
|
title: 'Preview',
|
||||||
|
description:
|
||||||
|
'Every push gets a live preview URL. Share it, comment on it, and ship with confidence — no environment drift, no surprises.',
|
||||||
|
icon: (
|
||||||
|
<>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="4"
|
||||||
|
width="18"
|
||||||
|
height="14"
|
||||||
|
rx="2"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path d="M9 21h6M12 18v3" strokeWidth="1.5" fill="none" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '03',
|
||||||
|
title: 'Ship',
|
||||||
|
description:
|
||||||
|
'Deploy to a global edge network in seconds. Instant rollbacks, zero-downtime releases, and traffic that lands close to every user.',
|
||||||
|
icon: (
|
||||||
|
<path
|
||||||
|
d="M4 13l1.5-5.5A2 2 0 0 1 7.4 6h9.2a2 2 0 0 1 1.9 1.5L20 13M4 13v5h16v-5M4 13h16M8 17h.01M16 17h.01"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function FeaturesSection() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<Container className="py-24">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="The workflow"
|
||||||
|
title="From commit to production, without the ceremony."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-14 grid gap-px overflow-hidden rounded-2xl border border-border bg-border md:grid-cols-3">
|
||||||
|
{features.map((feature) => (
|
||||||
|
<div
|
||||||
|
key={feature.step}
|
||||||
|
className="group flex flex-col bg-background p-8 transition-colors hover:bg-foreground/[0.03]"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<svg
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="text-foreground/80"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{feature.icon}
|
||||||
|
</svg>
|
||||||
|
<span className="font-mono text-xs text-foreground/30">
|
||||||
|
{feature.step}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-6 text-xl font-medium">{feature.title}</h3>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-foreground/50">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
Snippet,
|
||||||
|
SnippetAddon,
|
||||||
|
SnippetCopyButton,
|
||||||
|
SnippetInput,
|
||||||
|
SnippetText,
|
||||||
|
} from '@/components/ai-elements/snippet';
|
||||||
|
import { ActionButtons } from '@/components/shared/action-buttons';
|
||||||
|
import { Container } from '@/components/shared/container';
|
||||||
|
import { GradientGlow } from '@/components/shared/gradient-glow';
|
||||||
|
|
||||||
|
const hero = {
|
||||||
|
announcement: { label: 'Vertex Edge is now generally available', href: '#' },
|
||||||
|
title: 'Build. Preview. Ship.',
|
||||||
|
description:
|
||||||
|
'Vertex is the frontend cloud. Give your team the tooling and global infrastructure to build, preview, and ship a faster web — from your first commit to production traffic.',
|
||||||
|
primary: { label: 'Start Deploying', href: '#' },
|
||||||
|
secondary: { label: 'Get a Demo', href: '#' },
|
||||||
|
command: 'vertex deploy --prod',
|
||||||
|
};
|
||||||
|
|
||||||
|
function GridBackdrop() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,black,transparent)]"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(to right, color-mix(in oklab, var(--foreground) 6%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in oklab, var(--foreground) 6%, transparent) 1px, transparent 1px)',
|
||||||
|
backgroundSize: '56px 56px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroSection() {
|
||||||
|
return (
|
||||||
|
<section className="relative overflow-hidden">
|
||||||
|
<GridBackdrop />
|
||||||
|
<GradientGlow className="left-1/2 top-[-120px] h-[420px] w-[820px] -translate-x-1/2 opacity-40" />
|
||||||
|
|
||||||
|
<Container className="relative pt-24 pb-20 text-center sm:pt-32">
|
||||||
|
<a
|
||||||
|
href={hero.announcement.href}
|
||||||
|
className="group inline-flex items-center gap-2 rounded-full border border-foreground/15 bg-foreground/5 px-3.5 py-1.5 text-xs text-foreground/70 transition-colors hover:border-foreground/25 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
||||||
|
{hero.announcement.label}
|
||||||
|
<span className="transition-transform group-hover:translate-x-0.5">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h1 className="mx-auto mt-8 max-w-4xl bg-gradient-to-b from-foreground to-foreground/60 bg-clip-text text-5xl font-semibold leading-[1.05] tracking-tight text-transparent sm:text-7xl">
|
||||||
|
{hero.title}
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-foreground/55">
|
||||||
|
{hero.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ActionButtons
|
||||||
|
className="mt-10"
|
||||||
|
primary={hero.primary}
|
||||||
|
secondary={hero.secondary}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Snippet
|
||||||
|
className="mx-auto mt-14 h-12 max-w-md rounded-xl border-border bg-foreground/[0.03] text-sm"
|
||||||
|
code={hero.command}
|
||||||
|
>
|
||||||
|
<SnippetAddon className="pl-1">
|
||||||
|
<SnippetText>$</SnippetText>
|
||||||
|
</SnippetAddon>
|
||||||
|
<SnippetInput />
|
||||||
|
<SnippetAddon align="inline-end" className="pr-2">
|
||||||
|
<SnippetCopyButton />
|
||||||
|
</SnippetAddon>
|
||||||
|
</Snippet>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import { Container } from '@/components/shared/container';
|
||||||
import { Marquee } from '@/components/ui/marquee';
|
import { Marquee } from '@/components/ui/marquee';
|
||||||
|
|
||||||
const customerLogos = [
|
const customerLogos = [
|
||||||
@@ -53,11 +54,11 @@ const customerLogos = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function LogoCloud() {
|
export function LogosSection() {
|
||||||
return (
|
return (
|
||||||
<section className="border-y border-white/10 bg-white/[0.02]">
|
<section className="border-y border-border bg-foreground/[0.02]">
|
||||||
<div className="mx-auto max-w-6xl px-6 py-12">
|
<Container className="py-12">
|
||||||
<p className="text-center text-xs font-medium uppercase tracking-[0.2em] text-white/40">
|
<p className="text-center text-xs font-medium uppercase tracking-[0.2em] text-foreground/40">
|
||||||
Trusted by fast-moving teams shipping to millions
|
Trusted by fast-moving teams shipping to millions
|
||||||
</p>
|
</p>
|
||||||
<div className="relative mt-8 overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_15%,black_85%,transparent)]">
|
<div className="relative mt-8 overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_15%,black_85%,transparent)]">
|
||||||
@@ -73,13 +74,13 @@ export function LogoCloud() {
|
|||||||
width={logo.width}
|
width={logo.width}
|
||||||
height={logo.height}
|
height={logo.height}
|
||||||
unoptimized
|
unoptimized
|
||||||
className="h-7 w-[120px] max-w-full object-contain opacity-40 brightness-0 invert transition-opacity duration-200 group-hover:opacity-80"
|
className="h-7 w-[120px] max-w-full object-contain opacity-40 brightness-0 transition-opacity duration-200 group-hover:opacity-80 dark:invert"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Marquee>
|
</Marquee>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { StatsGlobe } from '@/components/site/stats-globe';
|
import { StatsGlobe } from '@/components/sections/stats/stats-globe';
|
||||||
|
import { Container } from '@/components/shared/container';
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ value: '45ms', label: 'Median edge response, worldwide' },
|
{ value: '45ms', label: 'Median edge response, worldwide' },
|
||||||
@@ -7,25 +8,25 @@ const stats = [
|
|||||||
{ value: '1.2T', label: 'Requests served every month' },
|
{ value: '1.2T', label: 'Requests served every month' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Stats() {
|
export function StatsSection() {
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden border-y border-white/10 bg-black py-16 sm:py-24">
|
<section className="relative overflow-hidden border-y border-border bg-background py-16 sm:py-24">
|
||||||
<StatsGlobe />
|
<StatsGlobe />
|
||||||
<div className="relative z-10 mx-auto grid max-w-6xl grid-cols-2 gap-px lg:grid-cols-4">
|
<Container className="relative z-10 grid grid-cols-2 gap-px px-0 lg:grid-cols-4">
|
||||||
{stats.map((stat) => (
|
{stats.map((stat) => (
|
||||||
<div
|
<div
|
||||||
key={stat.label}
|
key={stat.label}
|
||||||
className="px-6 py-12 text-center ring-1 ring-white/10"
|
className="px-6 py-12 text-center ring-1 ring-border"
|
||||||
>
|
>
|
||||||
<div className="text-4xl font-semibold tracking-tight sm:text-5xl">
|
<div className="text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
{stat.value}
|
{stat.value}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mt-3 max-w-[16ch] text-sm text-white/45">
|
<div className="mx-auto mt-3 max-w-[16ch] text-sm text-foreground/45">
|
||||||
{stat.label}
|
{stat.label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { type COBEOptions } from 'cobe';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
|
import { Globe } from '@/components/ui/globe';
|
||||||
|
|
||||||
|
// Rotation per frame. Default globe is 0.005; lower is slower, 0 stops it.
|
||||||
|
const GLOBE_SPEED = 0.003;
|
||||||
|
|
||||||
|
const markers: COBEOptions['markers'] = [
|
||||||
|
{ location: [37.7749, -122.4194], size: 0.08 },
|
||||||
|
{ location: [40.7128, -74.006], size: 0.06 },
|
||||||
|
{ location: [51.5072, -0.1276], size: 0.07 },
|
||||||
|
{ location: [48.8566, 2.3522], size: 0.05 },
|
||||||
|
{ location: [1.3521, 103.8198], size: 0.08 },
|
||||||
|
{ location: [35.6762, 139.6503], size: 0.06 },
|
||||||
|
{ location: [-33.8688, 151.2093], size: 0.05 },
|
||||||
|
{ location: [-23.5505, -46.6333], size: 0.06 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const darkGlobeConfig: COBEOptions = {
|
||||||
|
width: 800,
|
||||||
|
height: 800,
|
||||||
|
onRender: () => {},
|
||||||
|
devicePixelRatio: 2,
|
||||||
|
phi: 0,
|
||||||
|
theta: 0.22,
|
||||||
|
dark: 1,
|
||||||
|
diffuse: 1.2,
|
||||||
|
mapSamples: 20000,
|
||||||
|
mapBrightness: 5,
|
||||||
|
baseColor: [0.22, 0.22, 0.22],
|
||||||
|
markerColor: [1, 1, 1],
|
||||||
|
glowColor: [0.1, 0.1, 0.1],
|
||||||
|
markers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const lightGlobeConfig: COBEOptions = {
|
||||||
|
...darkGlobeConfig,
|
||||||
|
dark: 0,
|
||||||
|
diffuse: 0.4,
|
||||||
|
mapBrightness: 1.2,
|
||||||
|
baseColor: [0.88, 0.88, 0.88],
|
||||||
|
markerColor: [0.1, 0.1, 0.1],
|
||||||
|
glowColor: [0.96, 0.96, 0.96],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StatsGlobe() {
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-0 overflow-hidden [mask-image:radial-gradient(ellipse_80%_110%_at_50%_90%,black_45%,transparent_100%)]"
|
||||||
|
>
|
||||||
|
<div className="absolute left-1/2 top-[6%] aspect-square w-[760px] -translate-x-1/2 opacity-90 sm:w-[1040px] lg:w-[1280px]">
|
||||||
|
<Globe
|
||||||
|
className="max-w-none"
|
||||||
|
config={resolvedTheme === 'light' ? lightGlobeConfig : darkGlobeConfig}
|
||||||
|
speed={GLOBE_SPEED}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-background/80 to-transparent" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type Action = { label: string; href: string };
|
||||||
|
|
||||||
|
export function ActionButtons({
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
primary: Action;
|
||||||
|
secondary?: Action;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center justify-center gap-3 sm:flex-row',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
className="h-11 rounded-full bg-foreground px-6 text-[15px] text-background hover:bg-foreground/90"
|
||||||
|
>
|
||||||
|
<a href={primary.href}>{primary.label}</a>
|
||||||
|
</Button>
|
||||||
|
{secondary && (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
className="h-11 rounded-full border-foreground/20 bg-transparent px-6 text-[15px] text-foreground hover:bg-foreground/5 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<a href={secondary.href}>{secondary.label}</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { siteConfig } from '@/lib/site';
|
||||||
|
|
||||||
export function Logo({ className }: { className?: string }) {
|
export function Brand({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<span className={cn('flex items-center gap-2 font-medium', className)}>
|
<span className={cn('flex items-center gap-2 font-medium', className)}>
|
||||||
<svg
|
<svg
|
||||||
@@ -13,7 +14,7 @@ export function Logo({ className }: { className?: string }) {
|
|||||||
>
|
>
|
||||||
<path d="M12 2L23 21H1L12 2Z" fill="currentColor" />
|
<path d="M12 2L23 21H1L12 2Z" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-[17px] tracking-tight">Vertex</span>
|
<span className="text-[17px] tracking-tight">{siteConfig.name}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export function Container({ className, ...props }: ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div className={cn('mx-auto w-full max-w-6xl px-6', className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft conic-gradient bloom used behind hero and CTA copy.
|
||||||
|
* Position and size it with `className`; it never captures pointer events.
|
||||||
|
*/
|
||||||
|
export function GradientGlow({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none absolute rounded-full blur-3xl',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'conic-gradient(from 180deg at 50% 50%, #7c3aed, #2563eb, #06b6d4, #ec4899, #7c3aed)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export function SectionHeading({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
align = 'left',
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
eyebrow?: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
align?: 'left' | 'center';
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'max-w-2xl',
|
||||||
|
align === 'center' && 'mx-auto text-center',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{eyebrow && (
|
||||||
|
<p className="font-mono text-xs uppercase tracking-[0.2em] text-foreground/40">
|
||||||
|
{eyebrow}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h2
|
||||||
|
className={cn(
|
||||||
|
'text-3xl font-semibold tracking-tight sm:text-4xl',
|
||||||
|
eyebrow && 'mt-4',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-5 text-lg leading-8 text-foreground/55">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
|
||||||
|
|
||||||
export function Cta() {
|
|
||||||
return (
|
|
||||||
<section className="relative mx-auto max-w-6xl overflow-hidden px-6 py-28 text-center">
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none absolute left-1/2 top-1/2 h-[300px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full opacity-25 blur-3xl"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
'conic-gradient(from 180deg at 50% 50%, #7c3aed, #2563eb, #06b6d4, #ec4899, #7c3aed)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<h2 className="relative mx-auto max-w-2xl text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
||||||
Ship your first project in minutes.
|
|
||||||
</h2>
|
|
||||||
<p className="relative mx-auto mt-5 max-w-xl text-lg text-white/55">
|
|
||||||
Import a repository or start from a template. No credit card required to
|
|
||||||
deploy your first preview.
|
|
||||||
</p>
|
|
||||||
<div className="relative mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size="lg"
|
|
||||||
className="h-11 rounded-full bg-white px-6 text-[15px] text-black hover:bg-white/90"
|
|
||||||
>
|
|
||||||
<a href="#">Start Deploying</a>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
className="h-11 rounded-full border-white/20 bg-transparent px-6 text-[15px] text-white hover:bg-white/5 hover:text-white"
|
|
||||||
>
|
|
||||||
<a href="#">Talk to Sales</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
const features = [
|
|
||||||
{
|
|
||||||
step: '01',
|
|
||||||
title: 'Build',
|
|
||||||
description:
|
|
||||||
'Native support for 35+ frameworks. Push your code and Vertex handles the build pipeline, caching, and dependencies automatically.',
|
|
||||||
icon: <path d="M8 6L2 12l6 6M16 6l6 6-6 6" strokeWidth="1.5" fill="none" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: '02',
|
|
||||||
title: 'Preview',
|
|
||||||
description:
|
|
||||||
'Every push gets a live preview URL. Share it, comment on it, and ship with confidence — no environment drift, no surprises.',
|
|
||||||
icon: (
|
|
||||||
<>
|
|
||||||
<rect
|
|
||||||
x="3"
|
|
||||||
y="4"
|
|
||||||
width="18"
|
|
||||||
height="14"
|
|
||||||
rx="2"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
<path d="M9 21h6M12 18v3" strokeWidth="1.5" fill="none" />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: '03',
|
|
||||||
title: 'Ship',
|
|
||||||
description:
|
|
||||||
'Deploy to a global edge network in seconds. Instant rollbacks, zero-downtime releases, and traffic that lands close to every user.',
|
|
||||||
icon: (
|
|
||||||
<path
|
|
||||||
d="M4 13l1.5-5.5A2 2 0 0 1 7.4 6h9.2a2 2 0 0 1 1.9 1.5L20 13M4 13v5h16v-5M4 13h16M8 17h.01M16 17h.01"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Features() {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto max-w-6xl px-6 py-24">
|
|
||||||
<div className="max-w-2xl">
|
|
||||||
<p className="font-mono text-xs uppercase tracking-[0.2em] text-white/40">
|
|
||||||
The workflow
|
|
||||||
</p>
|
|
||||||
<h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
||||||
From commit to production, without the ceremony.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-14 grid gap-px overflow-hidden rounded-2xl border border-white/10 bg-white/10 md:grid-cols-3">
|
|
||||||
{features.map((feature) => (
|
|
||||||
<div
|
|
||||||
key={feature.step}
|
|
||||||
className="group flex flex-col bg-black p-8 transition-colors hover:bg-white/[0.03]"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<svg
|
|
||||||
width="28"
|
|
||||||
height="28"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
className="text-white/80"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{feature.icon}
|
|
||||||
</svg>
|
|
||||||
<span className="font-mono text-xs text-white/30">
|
|
||||||
{feature.step}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h3 className="mt-6 text-xl font-medium">{feature.title}</h3>
|
|
||||||
<p className="mt-3 text-sm leading-6 text-white/50">
|
|
||||||
{feature.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { RiGithubFill, RiTwitterXFill } from '@remixicon/react';
|
|
||||||
|
|
||||||
import { Logo } from '@/components/site/logo';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
heading: 'Products',
|
|
||||||
links: ['Previews', 'Edge Network', 'Analytics', 'Storage', 'Firewall'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: 'Resources',
|
|
||||||
links: ['Docs', 'Guides', 'Changelog', 'Templates', 'Status'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: 'Company',
|
|
||||||
links: ['About', 'Careers', 'Blog', 'Customers', 'Contact'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: 'Legal',
|
|
||||||
links: ['Privacy', 'Terms', 'Security', 'DPA', 'Cookies'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Footer() {
|
|
||||||
return (
|
|
||||||
<footer className="border-t border-white/10">
|
|
||||||
<div className="mx-auto max-w-6xl px-6 py-16">
|
|
||||||
<div className="grid gap-10 md:grid-cols-[1.5fr_repeat(4,1fr)]">
|
|
||||||
<div>
|
|
||||||
<Logo />
|
|
||||||
<p className="mt-4 max-w-xs text-sm text-white/45">
|
|
||||||
The frontend cloud for teams that ship.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{columns.map((column) => (
|
|
||||||
<div key={column.heading}>
|
|
||||||
<h4 className="text-sm font-medium text-white/80">
|
|
||||||
{column.heading}
|
|
||||||
</h4>
|
|
||||||
<ul className="mt-4 space-y-3">
|
|
||||||
{column.links.map((link) => (
|
|
||||||
<li key={link}>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-sm text-white/45 transition-colors hover:text-white"
|
|
||||||
>
|
|
||||||
{link}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-14 flex flex-col items-start justify-between gap-4 border-t border-white/10 pt-8 text-sm text-white/40 sm:flex-row sm:items-center">
|
|
||||||
<span>© 2026 Vertex, Inc. All rights reserved.</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<a
|
|
||||||
href="https://github.com"
|
|
||||||
aria-label="GitHub"
|
|
||||||
className="inline-flex size-8 items-center justify-center rounded-md text-white/45 transition-colors hover:bg-white/5 hover:text-white"
|
|
||||||
>
|
|
||||||
<RiGithubFill className="size-4" aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://x.com"
|
|
||||||
aria-label="X (Twitter)"
|
|
||||||
className="inline-flex size-8 items-center justify-center rounded-md text-white/45 transition-colors hover:bg-white/5 hover:text-white"
|
|
||||||
>
|
|
||||||
<RiTwitterXFill className="size-4" aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
|
||||||
|
|
||||||
export function Hero() {
|
|
||||||
return (
|
|
||||||
<section className="relative overflow-hidden">
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,black,transparent)]"
|
|
||||||
style={{
|
|
||||||
backgroundImage:
|
|
||||||
'linear-gradient(to right, rgba(255,255,255,0.06) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.06) 1px, transparent 1px)',
|
|
||||||
backgroundSize: '56px 56px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none absolute left-1/2 top-[-120px] h-[420px] w-[820px] -translate-x-1/2 rounded-full opacity-40 blur-3xl"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
'conic-gradient(from 180deg at 50% 50%, #7c3aed, #2563eb, #06b6d4, #ec4899, #7c3aed)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative mx-auto max-w-6xl px-6 pt-24 pb-20 text-center sm:pt-32">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="group inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-3.5 py-1.5 text-xs text-white/70 transition-colors hover:border-white/25 hover:text-white"
|
|
||||||
>
|
|
||||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
|
||||||
Vertex Edge is now generally available
|
|
||||||
<span className="transition-transform group-hover:translate-x-0.5">
|
|
||||||
→
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<h1 className="mx-auto mt-8 max-w-4xl bg-gradient-to-b from-white to-white/60 bg-clip-text text-5xl font-semibold leading-[1.05] tracking-tight text-transparent sm:text-7xl">
|
|
||||||
Build. Preview. Ship.
|
|
||||||
</h1>
|
|
||||||
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-white/55">
|
|
||||||
Vertex is the frontend cloud. Give your team the tooling and global
|
|
||||||
infrastructure to build, preview, and ship a faster web — from your
|
|
||||||
first commit to production traffic.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size="lg"
|
|
||||||
className="h-11 rounded-full bg-white px-6 text-[15px] text-black hover:bg-white/90"
|
|
||||||
>
|
|
||||||
<a href="#">Start Deploying</a>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
className="h-11 rounded-full border-white/20 bg-transparent px-6 text-[15px] text-white hover:bg-white/5 hover:text-white"
|
|
||||||
>
|
|
||||||
<a href="#">Get a Demo</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx-auto mt-14 flex max-w-md items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-4 py-3 font-mono text-sm text-white/70">
|
|
||||||
<span className="text-white/30">$</span>
|
|
||||||
<span>
|
|
||||||
<span className="text-white">vertex</span> deploy --prod
|
|
||||||
</span>
|
|
||||||
<span className="ml-auto h-4 w-px animate-pulse bg-white/40" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { Logo } from '@/components/site/logo';
|
|
||||||
import { MobileNavigation } from '@/components/site/mobile-navigation';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
|
|
||||||
const navigationItems = [
|
|
||||||
{ label: 'Products', href: '#', description: 'Everything you need to ship' },
|
|
||||||
{ label: 'Solutions', href: '#', description: 'Built for teams of every size' },
|
|
||||||
{ label: 'Resources', href: '#', description: 'Guides, templates, and changelog' },
|
|
||||||
{ label: 'Docs', href: '#', description: 'Your first deploy starts here' },
|
|
||||||
{ label: 'Pricing', href: '#', description: 'Start free. Scale at your pace.' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Navbar() {
|
|
||||||
return (
|
|
||||||
<header className="sticky top-0 z-50 border-b border-white/10 bg-black/70 backdrop-blur-xl">
|
|
||||||
<nav className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<Logo />
|
|
||||||
<ul className="hidden items-center gap-6 md:flex">
|
|
||||||
{navigationItems.map(({ label, href }) => (
|
|
||||||
<li key={label}>
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
className="text-sm text-white/60 transition-colors hover:text-white"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size="lg"
|
|
||||||
variant="ghost"
|
|
||||||
className="hidden text-white/70 hover:bg-white/5 hover:text-white sm:inline-flex"
|
|
||||||
>
|
|
||||||
<a href="#">Log In</a>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size="lg"
|
|
||||||
className="hidden rounded-full bg-white text-black hover:bg-white/90 sm:inline-flex"
|
|
||||||
>
|
|
||||||
<a href="#">Sign Up</a>
|
|
||||||
</Button>
|
|
||||||
<MobileNavigation items={navigationItems} />
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { type COBEOptions } from 'cobe';
|
|
||||||
|
|
||||||
import { Globe } from '@/components/ui/globe';
|
|
||||||
|
|
||||||
// Rotation per frame. Default globe is 0.005; lower is slower, 0 stops it.
|
|
||||||
const GLOBE_SPEED = 0.003;
|
|
||||||
|
|
||||||
const globeConfig: COBEOptions = {
|
|
||||||
width: 800,
|
|
||||||
height: 800,
|
|
||||||
onRender: () => {},
|
|
||||||
devicePixelRatio: 2,
|
|
||||||
phi: 0,
|
|
||||||
theta: 0.22,
|
|
||||||
dark: 1,
|
|
||||||
diffuse: 1.2,
|
|
||||||
mapSamples: 20000,
|
|
||||||
mapBrightness: 5,
|
|
||||||
baseColor: [0.22, 0.22, 0.22],
|
|
||||||
markerColor: [1, 1, 1],
|
|
||||||
glowColor: [0.1, 0.1, 0.1],
|
|
||||||
markers: [
|
|
||||||
{ location: [37.7749, -122.4194], size: 0.08 },
|
|
||||||
{ location: [40.7128, -74.006], size: 0.06 },
|
|
||||||
{ location: [51.5072, -0.1276], size: 0.07 },
|
|
||||||
{ location: [48.8566, 2.3522], size: 0.05 },
|
|
||||||
{ location: [1.3521, 103.8198], size: 0.08 },
|
|
||||||
{ location: [35.6762, 139.6503], size: 0.06 },
|
|
||||||
{ location: [-33.8688, 151.2093], size: 0.05 },
|
|
||||||
{ location: [-23.5505, -46.6333], size: 0.06 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function StatsGlobe() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none absolute inset-0 overflow-hidden [mask-image:radial-gradient(ellipse_80%_110%_at_50%_90%,black_45%,transparent_100%)]"
|
|
||||||
>
|
|
||||||
<div className="absolute left-1/2 top-[6%] aspect-square w-[760px] -translate-x-1/2 opacity-90 sm:w-[1040px] lg:w-[1280px]">
|
|
||||||
<Globe className="max-w-none" config={globeConfig} speed={GLOBE_SPEED} />
|
|
||||||
</div>
|
|
||||||
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-black/80 to-transparent" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
export const siteConfig = {
|
||||||
|
name: 'Vertex',
|
||||||
|
title: 'Vertex — Build. Preview. Ship.',
|
||||||
|
description:
|
||||||
|
'Vertex gives frontend teams the tooling and cloud infrastructure to build, preview, and ship a faster web.',
|
||||||
|
tagline: 'The frontend cloud for teams that ship.',
|
||||||
|
links: {
|
||||||
|
github: 'https://github.com',
|
||||||
|
twitter: 'https://x.com',
|
||||||
|
login: '#',
|
||||||
|
signup: '#',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationItem = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navigationItems: NavigationItem[] = [
|
||||||
|
{ label: 'Products', href: '#', description: 'Everything you need to ship' },
|
||||||
|
{ label: 'Solutions', href: '#', description: 'Built for teams of every size' },
|
||||||
|
{ label: 'Resources', href: '#', description: 'Guides, templates, and changelog' },
|
||||||
|
{ label: 'Docs', href: '#', description: 'Your first deploy starts here' },
|
||||||
|
{ label: 'Pricing', href: '#', description: 'Start free. Scale at your pace.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export type FooterColumn = {
|
||||||
|
heading: string;
|
||||||
|
links: { label: string; href: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const link = (label: string, href = '#') => ({ label, href });
|
||||||
|
|
||||||
|
export const footerColumns: FooterColumn[] = [
|
||||||
|
{
|
||||||
|
heading: 'Products',
|
||||||
|
links: ['Previews', 'Edge Network', 'Analytics', 'Storage', 'Firewall'].map((l) => link(l)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Resources',
|
||||||
|
links: ['Docs', 'Guides', 'Changelog', 'Templates', 'Status'].map((l) => link(l)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Company',
|
||||||
|
links: ['About', 'Careers', 'Blog', 'Customers', 'Contact'].map((l) => link(l)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Legal',
|
||||||
|
links: ['Privacy', 'Terms', 'Security', 'DPA', 'Cookies'].map((l) => link(l)),
|
||||||
|
},
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user