hero1
This commit is contained in:
parent
42b158a6ed
commit
b16926d1bb
9 changed files with 578 additions and 30 deletions
|
|
@ -42,6 +42,7 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
|
"framer-motion": "^12.40.0",
|
||||||
"geist": "^1.3.0",
|
"geist": "^1.3.0",
|
||||||
"graphql": "^16.8.2",
|
"graphql": "^16.8.2",
|
||||||
"lucide-react": "0.563.0",
|
"lucide-react": "0.563.0",
|
||||||
|
|
@ -52,6 +53,7 @@
|
||||||
"react": "19.2.6",
|
"react": "19.2.6",
|
||||||
"react-dom": "19.2.6",
|
"react-dom": "19.2.6",
|
||||||
"react-hook-form": "7.71.1",
|
"react-hook-form": "7.71.1",
|
||||||
|
"react-use-measure": "^2.1.7",
|
||||||
"sharp": "0.34.2",
|
"sharp": "0.34.2",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
|
|
@ -68,6 +68,9 @@ importers:
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: 16.4.7
|
specifier: 16.4.7
|
||||||
version: 16.4.7
|
version: 16.4.7
|
||||||
|
framer-motion:
|
||||||
|
specifier: ^12.40.0
|
||||||
|
version: 12.40.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
geist:
|
geist:
|
||||||
specifier: ^1.3.0
|
specifier: ^1.3.0
|
||||||
version: 1.7.1(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))
|
version: 1.7.1(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))
|
||||||
|
|
@ -98,6 +101,9 @@ importers:
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: 7.71.1
|
specifier: 7.71.1
|
||||||
version: 7.71.1(react@19.2.6)
|
version: 7.71.1(react@19.2.6)
|
||||||
|
react-use-measure:
|
||||||
|
specifier: ^2.1.7
|
||||||
|
version: 2.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
sharp:
|
sharp:
|
||||||
specifier: 0.34.2
|
specifier: 0.34.2
|
||||||
version: 0.34.2
|
version: 0.34.2
|
||||||
|
|
@ -3057,6 +3063,20 @@ packages:
|
||||||
fraction.js@5.3.4:
|
fraction.js@5.3.4:
|
||||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||||
|
|
||||||
|
framer-motion@12.40.0:
|
||||||
|
resolution: {integrity: sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.2:
|
fsevents@2.3.2:
|
||||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
|
@ -3809,6 +3829,12 @@ packages:
|
||||||
resolution: {integrity: sha512-RhQ4DzmBi5BNGcS0w4u1vdMRIKcteXTCNzDt1j7XRcdWYBz1MjMjulBhPaeC5jBCHOD1yinuOFTTSOWLLGexWw==}
|
resolution: {integrity: sha512-RhQ4DzmBi5BNGcS0w4u1vdMRIKcteXTCNzDt1j7XRcdWYBz1MjMjulBhPaeC5jBCHOD1yinuOFTTSOWLLGexWw==}
|
||||||
engines: {node: '>=16.20.1'}
|
engines: {node: '>=16.20.1'}
|
||||||
|
|
||||||
|
motion-dom@12.40.0:
|
||||||
|
resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==}
|
||||||
|
|
||||||
|
motion-utils@12.39.0:
|
||||||
|
resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
|
||||||
|
|
||||||
mpath@0.8.4:
|
mpath@0.8.4:
|
||||||
resolution: {integrity: sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==}
|
resolution: {integrity: sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
@ -4183,6 +4209,15 @@ packages:
|
||||||
react: '>=16.6.0'
|
react: '>=16.6.0'
|
||||||
react-dom: '>=16.6.0'
|
react-dom: '>=16.6.0'
|
||||||
|
|
||||||
|
react-use-measure@2.1.7:
|
||||||
|
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.13'
|
||||||
|
react-dom: '>=16.13'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react@19.2.6:
|
react@19.2.6:
|
||||||
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
|
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -8081,6 +8116,15 @@ snapshots:
|
||||||
|
|
||||||
fraction.js@5.3.4: {}
|
fraction.js@5.3.4: {}
|
||||||
|
|
||||||
|
framer-motion@12.40.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.40.0
|
||||||
|
motion-utils: 12.39.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.6
|
||||||
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
|
||||||
fsevents@2.3.2:
|
fsevents@2.3.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -8935,6 +8979,12 @@ snapshots:
|
||||||
- socks
|
- socks
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
motion-dom@12.40.0:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.39.0
|
||||||
|
|
||||||
|
motion-utils@12.39.0: {}
|
||||||
|
|
||||||
mpath@0.8.4: {}
|
mpath@0.8.4: {}
|
||||||
|
|
||||||
mpath@0.9.0: {}
|
mpath@0.9.0: {}
|
||||||
|
|
@ -9362,6 +9412,12 @@ snapshots:
|
||||||
react: 19.2.6
|
react: 19.2.6
|
||||||
react-dom: 19.2.6(react@19.2.6)
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
|
||||||
|
react-use-measure@2.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.6
|
||||||
|
optionalDependencies:
|
||||||
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
|
||||||
react@19.2.6: {}
|
react@19.2.6: {}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,56 @@
|
||||||
import { cn } from '@/utilities/ui'
|
import * as React from "react"
|
||||||
import { Slot } from '@radix-ui/react-slot'
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { type VariantProps, cva } from 'class-variance-authority'
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import * as React from 'react'
|
|
||||||
|
import { cn } from "@/utilities/ui"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive: 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
secondary:
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
clear: '',
|
default: "h-10 px-4 py-2",
|
||||||
default: 'h-10 px-4 py-2 has-[>svg]:px-3',
|
sm: "h-9 rounded-md px-3",
|
||||||
sm: 'h-9 rounded-md px-3 has-[>svg]:px-2.5',
|
lg: "h-11 rounded-md px-8",
|
||||||
lg: 'h-11 rounded-md px-8 has-[>svg]:px-4',
|
icon: "h-10 w-10",
|
||||||
icon: 'size-10',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: "default",
|
||||||
size: 'default',
|
size: "default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ComponentProps<'button'>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: React.FC<ButtonProps> = ({ asChild = false, className, size, variant, ...props }) => {
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
const Comp = asChild ? Slot : 'button'
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
|
|
||||||
134
src/components/ui/header-1.tsx
Normal file
134
src/components/ui/header-1.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, buttonVariants } from '@/components/ui/button';
|
||||||
|
import { cn } from '@/utilities/ui';
|
||||||
|
import { MenuToggleIcon } from '@/components/ui/menu-toggle-icon';
|
||||||
|
import { useScroll } from '@/components/ui/use-scroll';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const scrolled = useScroll(10);
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
label: 'Features',
|
||||||
|
href: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pricing',
|
||||||
|
href: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'About',
|
||||||
|
href: '#',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={cn('sticky top-0 z-50 w-full border-b border-transparent', {
|
||||||
|
'bg-background/95 supports-[backdrop-filter]:bg-background/50 border-border backdrop-blur-lg':
|
||||||
|
scrolled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<nav className="mx-auto flex h-14 w-full max-w-5xl items-center justify-between px-4">
|
||||||
|
<div className="hover:bg-accent rounded-md p-2">
|
||||||
|
<WordmarkIcon className="h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="hidden items-center gap-2 md:flex">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a key={link.label} className={buttonVariants({ variant: 'ghost' })} href={link.href}>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<Button variant="outline">Sign In</Button>
|
||||||
|
<Button>Get Started</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
className="md:hidden"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
<MenuToggleIcon open={open} className="size-5" duration={300} />
|
||||||
|
</Button>
|
||||||
|
</nav>
|
||||||
|
<MobileMenu open={open} className="flex flex-col justify-between gap-2">
|
||||||
|
<div className="grid gap-y-2">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.label}
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
className: 'justify-start',
|
||||||
|
})}
|
||||||
|
href={link.href}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button variant="outline" className="w-full bg-transparent">
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
<Button className="w-full">Get Started</Button>
|
||||||
|
</div>
|
||||||
|
</MobileMenu>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type MobileMenuProps = React.ComponentProps<'div'> & {
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function MobileMenu({ open, children, className, ...props }: MobileMenuProps) {
|
||||||
|
if (!open || typeof window === 'undefined') return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
className={cn(
|
||||||
|
'bg-background/95 supports-[backdrop-filter]:bg-background/50 backdrop-blur-lg',
|
||||||
|
'fixed top-14 right-0 bottom-0 left-0 z-40 flex flex-col overflow-hidden border-y md:hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-slot={open ? 'open' : 'closed'}
|
||||||
|
className={cn(
|
||||||
|
'data-[slot=open]:animate-in data-[slot=open]:zoom-in-97 ease-out',
|
||||||
|
'size-full p-4',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const WordmarkIcon = (props: React.ComponentProps<"svg">) => (
|
||||||
|
<svg viewBox="0 0 84 24" fill="currentColor" {...props}>
|
||||||
|
<path d="M45.035 23.984c-1.34-.062-2.566-.441-3.777-1.16-1.938-1.152-3.465-3.187-4.02-5.36-.199-.784-.238-1.128-.234-2.058 0-.691.008-.87.062-1.207.23-1.5.852-2.883 1.852-4.144.297-.371 1.023-1.09 1.41-1.387 1.399-1.082 2.84-1.68 4.406-1.816.536-.047 1.528-.02 2.047.054 1.227.184 2.227.543 3.106 1.121 1.277.84 2.5 2.184 3.367 3.7.098.168.172.308.172.312-.004 0-1.047.723-2.32 1.598l-2.711 1.867c-.61.422-2.91 2.008-2.993 2.062l-.074.047-1-1.574c-.55-.867-1.008-1.594-1.012-1.61-.007-.019.922-.648 2.188-1.476 1.215-.793 2.2-1.453 2.191-1.46-.02-.032-.508-.27-.691-.34a5 5 0 0 0-.465-.13c-.371-.09-1.105-.125-1.426-.07-1.285.219-2.336 1.3-2.777 2.852-.215.761-.242 1.636-.074 2.355.129.527.383 1.102.691 1.543.234.332.727.82 1.047 1.031.664.434 1.195.586 1.969.555.613-.023 1.027-.129 1.64-.426 1.184-.574 2.16-1.554 2.828-2.843.122-.235.208-.372.227-.368.082.032 3.77 1.938 3.79 1.961.034.032-.407.93-.696 1.414a12 12 0 0 1-1.051 1.477c-.36.422-1.102 1.14-1.492 1.445a9.9 9.9 0 0 1-3.23 1.684 9.2 9.2 0 0 1-2.95.351M74.441 23.996c-1.488-.043-2.8-.363-4.066-.992-1.687-.848-2.992-2.14-3.793-3.774-.605-1.234-.863-2.402-.863-3.894.004-1.149.176-2.156.527-3.11.14-.378.531-1.171.75-1.515 1.078-1.703 2.758-2.934 4.805-3.524.847-.242 1.465-.332 2.433-.351 1.032-.024 1.743.055 2.48.277l.31.09.007 2.48c.004 1.364 0 2.481-.008 2.481a1 1 0 0 1-.12-.055c-.688-.347-2.09-.488-2.962-.296-.754.167-1.296.453-1.785.945a3.7 3.7 0 0 0-1.043 2.11c-.047.382-.02 1.109.055 1.437a3.4 3.4 0 0 0 .941 1.738c.75.75 1.715 1.102 2.875 1.05.645-.03 1.118-.14 1.563-.366q1.721-.864 2.02-3.145c.035-.293.042-1.266.042-7.957V0H84l-.012 8.434c-.008 7.851-.011 8.457-.054 8.757-.196 1.274-.586 2.25-1.301 3.243-1.293 1.808-3.555 3.07-6.145 3.437-.664.098-1.43.14-2.047.125M9.848 23.574a14 14 0 0 1-1.137-.152c-2.352-.426-4.555-1.781-6.117-3.774-.27-.335-.75-1.05-.95-1.406-1.156-2.047-1.695-4.27-1.64-6.77.047-1.995.43-3.66 1.23-5.316.524-1.086 1.04-1.87 1.793-2.715C4.567 1.72 6.652.535 8.793.171 9.68.02 10.093 0 12.297 0h1.789v5.441l-.961.016c-2.36.04-3.441.215-4.441.719-.836.414-1.278.879-1.895 1.976-.219.399-.535 1.02-.535 1.063 0 .02 1.285.027 3.918.027h3.914v5.113h-3.914c-2.54 0-3.918.008-3.918.028 0 .05.254.597.441.953.344.656.649 1.086 1.051 1.48.668.657 1.356.985 2.445 1.16.645.106 1.274.145 2.61.16l1.285.016v5.442l-2.055-.004a120 120 0 0 1-2.183-.016M16.469 14.715c0-5.504.011-9.04.031-9.29a5.54 5.54 0 0 1 1.527-3.48c.778-.82 1.922-1.457 3.118-1.734C21.915.035 22.422 0 24.39 0h1.652v4.914h-1.426c-1.324 0-1.445.004-1.644.055-.739.191-1.059.699-1.106 1.754l-.015.355h4.191v4.914h-4.184v11.602h-5.39ZM27.023 14.727c0-5.223.012-9.04.028-9.278.129-1.98 1.234-3.68 3.012-4.62.87-.462 1.777-.716 2.851-.802A61 61 0 0 1 34.945 0h1.649v4.914h-1.426c-1.32 0-1.441.004-1.64.055-.739.191-1.063.699-1.106 1.754l-.02.355h4.192v4.914H32.41v11.602h-5.387ZM55.48 15.406V7.22h4.66v1.363c0 1.3.005 1.363.051 1.363.04 0 .075-.054.133-.203.38-.98.969-1.68 1.711-2.031.563-.266 1.422-.43 2.492-.48l.414-.02v4.914l-.414.035c-.738.063-1.597.195-2.058.313-.297.082-.688.28-.875.449-.324.289-.532.703-.625 1.254-.094.547-.098.879-.098 5.144v4.274h-5.39Zm0 0" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
130
src/components/ui/hero-1.tsx
Normal file
130
src/components/ui/hero-1.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { cn } from "@/utilities/ui";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { RocketIcon, ArrowRightIcon, PhoneCallIcon } from "lucide-react";
|
||||||
|
import { LogoCloud } from "@/components/ui/logo-cloud-3";
|
||||||
|
|
||||||
|
export function HeroSection() {
|
||||||
|
return (
|
||||||
|
<section className="mx-auto w-full max-w-5xl">
|
||||||
|
{/* Top Shades */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 isolate hidden overflow-hidden contain-strict lg:block"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 -top-14 isolate -z-10 bg-[radial-gradient(35%_80%_at_49%_0%,--theme(--color-foreground/.08),transparent)] contain-strict" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* X Bold Faded Borders */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 mx-auto hidden min-h-screen w-full max-w-5xl lg:block"
|
||||||
|
>
|
||||||
|
<div className="mask-y-from-80% mask-y-to-100% absolute inset-y-0 left-0 z-10 h-full w-px bg-foreground/15" />
|
||||||
|
<div className="mask-y-from-80% mask-y-to-100% absolute inset-y-0 right-0 z-10 h-full w-px bg-foreground/15" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* main content */}
|
||||||
|
|
||||||
|
<div className="relative flex flex-col items-center justify-center gap-5 pt-32 pb-30">
|
||||||
|
{/* X Content Faded Borders */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 -z-1 size-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-y-0 left-4 w-px bg-linear-to-b from-transparent via-border to-border md:left-8" />
|
||||||
|
<div className="absolute inset-y-0 right-4 w-px bg-linear-to-b from-transparent via-border to-border md:right-8" />
|
||||||
|
<div className="absolute inset-y-0 left-8 w-px bg-linear-to-b from-transparent via-border/50 to-border/50 md:left-12" />
|
||||||
|
<div className="absolute inset-y-0 right-8 w-px bg-linear-to-b from-transparent via-border/50 to-border/50 md:right-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
className={cn(
|
||||||
|
"group mx-auto flex w-fit items-center gap-3 rounded-full border bg-card px-3 py-1 shadow",
|
||||||
|
"fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards transition-all delay-500 duration-500 ease-out"
|
||||||
|
)}
|
||||||
|
href="#link"
|
||||||
|
>
|
||||||
|
<RocketIcon className="size-3 text-muted-foreground" />
|
||||||
|
<span className="text-xs">shipped new features!</span>
|
||||||
|
<span className="block h-5 border-l" />
|
||||||
|
|
||||||
|
<ArrowRightIcon className="size-3 duration-150 ease-out group-hover:translate-x-1" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
"fade-in slide-in-from-bottom-10 animate-in text-balance fill-mode-backwards text-center text-4xl tracking-tight delay-100 duration-500 ease-out md:text-5xl lg:text-6xl",
|
||||||
|
"text-shadow-[0_0px_50px_theme(--color-foreground/.2)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Building Teams Help <br /> You Scale and Lead
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="fade-in slide-in-from-bottom-10 mx-auto max-w-md animate-in fill-mode-backwards text-center text-base text-foreground/80 tracking-wider delay-200 duration-500 ease-out sm:text-lg md:text-xl">
|
||||||
|
Conecting you with world-class talent <br /> to scale, innovate and
|
||||||
|
lead
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="fade-in slide-in-from-bottom-10 flex animate-in flex-row flex-wrap items-center justify-center gap-3 fill-mode-backwards pt-2 delay-300 duration-500 ease-out">
|
||||||
|
<Button className="rounded-full" size="lg" variant="secondary">
|
||||||
|
<PhoneCallIcon data-icon="inline-start" className="size-4 mr-2" />{" "}
|
||||||
|
Book a Call
|
||||||
|
</Button>
|
||||||
|
<Button className="rounded-full " size="lg">
|
||||||
|
Get started{" "}
|
||||||
|
<ArrowRightIcon
|
||||||
|
className="size-4 ms-2"data-icon="inline-end" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogosSection() {
|
||||||
|
return (
|
||||||
|
<section className="relative space-y-4 border-t pt-6 pb-10">
|
||||||
|
<h2 className="text-center font-medium text-lg text-muted-foreground tracking-tight md:text-xl">
|
||||||
|
Trusted by <span className="text-foreground">experts</span>
|
||||||
|
</h2>
|
||||||
|
<div className="relative z-10 mx-auto max-w-4xl">
|
||||||
|
<LogoCloud logos={logos} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logos = [
|
||||||
|
{
|
||||||
|
src: "https://storage.efferd.com/logo/nvidia-wordmark.svg",
|
||||||
|
alt: "Nvidia Logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://storage.efferd.com/logo/supabase-wordmark.svg",
|
||||||
|
alt: "Supabase Logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://storage.efferd.com/logo/openai-wordmark.svg",
|
||||||
|
alt: "OpenAI Logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://storage.efferd.com/logo/turso-wordmark.svg",
|
||||||
|
alt: "Turso Logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://storage.efferd.com/logo/vercel-wordmark.svg",
|
||||||
|
alt: "Vercel Logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://storage.efferd.com/logo/github-wordmark.svg",
|
||||||
|
alt: "GitHub Logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://storage.efferd.com/logo/claude-wordmark.svg",
|
||||||
|
alt: "Claude AI Logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://storage.efferd.com/logo/clerk-wordmark.svg",
|
||||||
|
alt: "Clerk Logo",
|
||||||
|
},
|
||||||
|
];
|
||||||
107
src/components/ui/infinite-slider.tsx
Normal file
107
src/components/ui/infinite-slider.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
'use client';
|
||||||
|
import { cn } from '@/utilities/ui';
|
||||||
|
import { useMotionValue, animate, motion } from 'framer-motion';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import useMeasure from 'react-use-measure';
|
||||||
|
|
||||||
|
type InfiniteSliderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
gap?: number;
|
||||||
|
duration?: number;
|
||||||
|
durationOnHover?: number;
|
||||||
|
direction?: 'horizontal' | 'vertical';
|
||||||
|
reverse?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InfiniteSlider({
|
||||||
|
children,
|
||||||
|
gap = 16,
|
||||||
|
duration = 25,
|
||||||
|
durationOnHover,
|
||||||
|
direction = 'horizontal',
|
||||||
|
reverse = false,
|
||||||
|
className,
|
||||||
|
}: InfiniteSliderProps) {
|
||||||
|
const [currentDuration, setCurrentDuration] = useState(duration);
|
||||||
|
const [ref, { width, height }] = useMeasure();
|
||||||
|
const translation = useMotionValue(0);
|
||||||
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let controls;
|
||||||
|
const size = direction === 'horizontal' ? width : height;
|
||||||
|
const contentSize = size + gap;
|
||||||
|
const from = reverse ? -contentSize / 2 : 0;
|
||||||
|
const to = reverse ? 0 : -contentSize / 2;
|
||||||
|
|
||||||
|
if (isTransitioning) {
|
||||||
|
controls = animate(translation, [translation.get(), to], {
|
||||||
|
ease: 'linear',
|
||||||
|
duration:
|
||||||
|
currentDuration * Math.abs((translation.get() - to) / contentSize),
|
||||||
|
onComplete: () => {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
setKey((prevKey) => prevKey + 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
controls = animate(translation, [from, to], {
|
||||||
|
ease: 'linear',
|
||||||
|
duration: currentDuration,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: 'loop',
|
||||||
|
repeatDelay: 0,
|
||||||
|
onRepeat: () => {
|
||||||
|
translation.set(from);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return controls?.stop;
|
||||||
|
}, [
|
||||||
|
key,
|
||||||
|
translation,
|
||||||
|
currentDuration,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
gap,
|
||||||
|
isTransitioning,
|
||||||
|
direction,
|
||||||
|
reverse,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const hoverProps = durationOnHover
|
||||||
|
? {
|
||||||
|
onHoverStart: () => {
|
||||||
|
setIsTransitioning(true);
|
||||||
|
setCurrentDuration(durationOnHover);
|
||||||
|
},
|
||||||
|
onHoverEnd: () => {
|
||||||
|
setIsTransitioning(true);
|
||||||
|
setCurrentDuration(duration);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('overflow-hidden', className)}>
|
||||||
|
<motion.div
|
||||||
|
className='flex w-max'
|
||||||
|
style={{
|
||||||
|
...(direction === 'horizontal'
|
||||||
|
? { x: translation }
|
||||||
|
: { y: translation }),
|
||||||
|
gap: `${gap}px`,
|
||||||
|
flexDirection: direction === 'horizontal' ? 'row' : 'column',
|
||||||
|
}}
|
||||||
|
ref={ref}
|
||||||
|
{...hoverProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/components/ui/logo-cloud-3.tsx
Normal file
39
src/components/ui/logo-cloud-3.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { InfiniteSlider } from "@/components/ui/infinite-slider";
|
||||||
|
import { cn } from "@/utilities/ui";
|
||||||
|
|
||||||
|
type Logo = {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogoCloudProps = React.ComponentProps<"div"> & {
|
||||||
|
logos: Logo[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LogoCloud({ className, logos, ...props }: LogoCloudProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden py-4 [mask-image:linear-gradient(to_right,transparent,black,transparent)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<InfiniteSlider gap={42} reverse speed={80} speedOnHover={25}>
|
||||||
|
{logos.map((logo) => (
|
||||||
|
<img
|
||||||
|
alt={logo.alt}
|
||||||
|
className="pointer-events-none h-4 select-none md:h-5 dark:brightness-0 dark:invert"
|
||||||
|
height={logo.height || "auto"}
|
||||||
|
key={`logo-${logo.alt}`}
|
||||||
|
loading="lazy"
|
||||||
|
src={logo.src}
|
||||||
|
width={logo.width || "auto"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</InfiniteSlider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/components/ui/menu-toggle-icon.tsx
Normal file
54
src/components/ui/menu-toggle-icon.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { cn } from '@/utilities/ui';
|
||||||
|
|
||||||
|
type MenuToggleProps = React.ComponentProps<'svg'> & {
|
||||||
|
open: boolean;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MenuToggleIcon({
|
||||||
|
open,
|
||||||
|
className,
|
||||||
|
fill = 'none',
|
||||||
|
stroke = 'currentColor',
|
||||||
|
strokeWidth = 2.5,
|
||||||
|
strokeLinecap = 'round',
|
||||||
|
strokeLinejoin = 'round',
|
||||||
|
duration = 500,
|
||||||
|
...props
|
||||||
|
}: MenuToggleProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill={fill}
|
||||||
|
stroke={stroke}
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
strokeLinecap={strokeLinecap}
|
||||||
|
strokeLinejoin={strokeLinejoin}
|
||||||
|
className={cn(
|
||||||
|
'transition-transform ease-in-out',
|
||||||
|
open && '-rotate-45',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transitionDuration: `${duration}ms`,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
className={cn(
|
||||||
|
'transition-all ease-in-out',
|
||||||
|
open
|
||||||
|
? '[stroke-dasharray:20_300] [stroke-dashoffset:-32.42px]'
|
||||||
|
: '[stroke-dasharray:12_63]',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transitionDuration: `${duration}ms`,
|
||||||
|
}}
|
||||||
|
d="M27 10 13 10C10.8 10 9 8.2 9 6 9 3.5 10.8 2 13 2 15.2 2 17 3.8 17 6L17 26C17 28.2 18.8 30 21 30 23.2 30 25 28.2 25 26 25 23.8 23.2 22 21 22L7 22"
|
||||||
|
/>
|
||||||
|
<path d="M7 16 27 16" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/components/ui/use-scroll.tsx
Normal file
22
src/components/ui/use-scroll.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function useScroll(threshold: number) {
|
||||||
|
const [scrolled, setScrolled] = React.useState(false);
|
||||||
|
|
||||||
|
const onScroll = React.useCallback(() => {
|
||||||
|
setScrolled(window.scrollY > threshold);
|
||||||
|
}, [threshold]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.addEventListener('scroll', onScroll);
|
||||||
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
|
}, [onScroll]);
|
||||||
|
|
||||||
|
// also check on first load
|
||||||
|
React.useEffect(() => {
|
||||||
|
onScroll();
|
||||||
|
}, [onScroll]);
|
||||||
|
|
||||||
|
return scrolled;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue