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",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"framer-motion": "^12.40.0",
|
||||
"geist": "^1.3.0",
|
||||
"graphql": "^16.8.2",
|
||||
"lucide-react": "0.563.0",
|
||||
|
|
@ -52,6 +53,7 @@
|
|||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"react-hook-form": "7.71.1",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"sharp": "0.34.2",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
|
|
|
|||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
|
|
@ -68,6 +68,9 @@ importers:
|
|||
dotenv:
|
||||
specifier: 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:
|
||||
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))
|
||||
|
|
@ -98,6 +101,9 @@ importers:
|
|||
react-hook-form:
|
||||
specifier: 7.71.1
|
||||
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:
|
||||
specifier: 0.34.2
|
||||
version: 0.34.2
|
||||
|
|
@ -3057,6 +3063,20 @@ packages:
|
|||
fraction.js@5.3.4:
|
||||
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:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
|
|
@ -3809,6 +3829,12 @@ packages:
|
|||
resolution: {integrity: sha512-RhQ4DzmBi5BNGcS0w4u1vdMRIKcteXTCNzDt1j7XRcdWYBz1MjMjulBhPaeC5jBCHOD1yinuOFTTSOWLLGexWw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
|
@ -4183,6 +4209,15 @@ packages:
|
|||
react: '>=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:
|
||||
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -8081,6 +8116,15 @@ snapshots:
|
|||
|
||||
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:
|
||||
optional: true
|
||||
|
||||
|
|
@ -8935,6 +8979,12 @@ snapshots:
|
|||
- socks
|
||||
- supports-color
|
||||
|
||||
motion-dom@12.40.0:
|
||||
dependencies:
|
||||
motion-utils: 12.39.0
|
||||
|
||||
motion-utils@12.39.0: {}
|
||||
|
||||
mpath@0.8.4: {}
|
||||
|
||||
mpath@0.9.0: {}
|
||||
|
|
@ -9362,6 +9412,12 @@ snapshots:
|
|||
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: {}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
|
|
|
|||
|
|
@ -1,52 +1,56 @@
|
|||
import { cn } from '@/utilities/ui'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/utilities/ui"
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
clear: '',
|
||||
default: 'h-10 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-9 rounded-md px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-11 rounded-md px-8 has-[>svg]:px-4',
|
||||
icon: 'size-10',
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ComponentProps<'button'>,
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({ asChild = false, className, size, variant, ...props }) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
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