hero
This commit is contained in:
parent
c8d2fde9a3
commit
58ea85a9c2
2 changed files with 72 additions and 185 deletions
|
|
@ -8,13 +8,31 @@ import type { Header } from '@/payload-types'
|
||||||
|
|
||||||
import { Logo } from '@/components/Logo/Logo'
|
import { Logo } from '@/components/Logo/Logo'
|
||||||
import { HeaderNav } from './Nav'
|
import { HeaderNav } from './Nav'
|
||||||
|
import { useThemeMode } from '@/hooks/useThemeMode'
|
||||||
|
|
||||||
interface HeaderClientProps {
|
interface HeaderClientProps {
|
||||||
data: Header
|
data: Header
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ToggleButton() {
|
||||||
|
const { isDark, toggle } = useThemeMode()
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggle}
|
||||||
|
className="flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-all"
|
||||||
|
style={{
|
||||||
|
border: isDark ? '1px solid rgba(255,255,255,0.2)' : '1px solid rgba(0,0,0,0.15)',
|
||||||
|
color: isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)',
|
||||||
|
background: 'transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isDark ? '☀ Light' : '☾ Dark'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
||||||
/* Storing the value in a useState to avoid hydration errors */
|
|
||||||
const [theme, setTheme] = useState<string | null>(null)
|
const [theme, setTheme] = useState<string | null>(null)
|
||||||
const { headerTheme, setHeaderTheme } = useHeaderTheme()
|
const { headerTheme, setHeaderTheme } = useHeaderTheme()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
@ -30,12 +48,15 @@ export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
||||||
}, [headerTheme])
|
}, [headerTheme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="container relative z-20 " {...(theme ? { 'data-theme': theme } : {})}>
|
<header className="container relative z-20" {...(theme ? { 'data-theme': theme } : {})}>
|
||||||
<div className="py-8 flex justify-between">
|
<div className="py-8 flex justify-between">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<Logo loading="eager" priority="high" className="invert dark:invert-0" />
|
<Logo loading="eager" priority="high" className="invert dark:invert-0" />
|
||||||
</Link>
|
</Link>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<HeaderNav data={data} />
|
<HeaderNav data={data} />
|
||||||
|
<ToggleButton />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { cn } from '@/utilities/ui'
|
import { cn } from '@/utilities/ui'
|
||||||
import { Button, buttonVariants } from '@/components/ui/button'
|
|
||||||
import { ArrowRightIcon, PhoneCallIcon } from 'lucide-react'
|
import { ArrowRightIcon, PhoneCallIcon } from 'lucide-react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { MenuToggleIcon } from '@/components/ui/menu-toggle-icon'
|
import { MenuToggleIcon } from '@/components/ui/menu-toggle-icon'
|
||||||
import { useScroll } from '@/components/ui/use-scroll'
|
import { useScroll } from '@/components/ui/use-scroll'
|
||||||
import { useThemeMode } from '@/hooks/useThemeMode'
|
import { useThemeMode } from '@/hooks/useThemeMode'
|
||||||
|
import { CMSLink } from '@/components/Link'
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
import type { Page } from '@/payload-types'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -162,123 +165,15 @@ function Background({ isDark }: { isDark: boolean }) {
|
||||||
return <canvas ref={canvasRef} style={{ display: 'block', width: '100%', height: '100%' }} />
|
return <canvas ref={canvasRef} style={{ display: 'block', width: '100%', height: '100%' }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Header ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function Header({ isDark, onToggle }: { isDark: boolean; onToggle: () => void }) {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const scrolled = useScroll(10)
|
|
||||||
|
|
||||||
const links = [
|
|
||||||
{ label: 'Work', href: '#work' },
|
|
||||||
{ label: 'About', href: '#about' },
|
|
||||||
{ label: 'Contact', href: '#contact' },
|
|
||||||
]
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.body.style.overflow = open ? 'hidden' : ''
|
|
||||||
return () => { document.body.style.overflow = '' }
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
const textColor = isDark ? 'text-white/80 hover:text-white' : 'text-black/70 hover:text-black'
|
|
||||||
const borderColor = isDark ? 'border-white/10' : 'border-black/10'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className={cn(
|
|
||||||
'sticky top-0 z-50 w-full border-b border-transparent transition-all duration-300',
|
|
||||||
scrolled && (isDark
|
|
||||||
? 'bg-black/60 border-white/10 backdrop-blur-lg'
|
|
||||||
: 'bg-white/60 border-black/10 backdrop-blur-lg')
|
|
||||||
)}>
|
|
||||||
<nav className="mx-auto flex h-14 w-full max-w-5xl items-center justify-between px-6">
|
|
||||||
{/* Wordmark */}
|
|
||||||
<span className={cn('font-semibold tracking-tight text-sm transition-colors', isDark ? 'text-white' : 'text-black')}>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Desktop links */}
|
|
||||||
<div className="hidden items-center gap-1 md:flex">
|
|
||||||
{links.map(l => (
|
|
||||||
<a key={l.label}
|
|
||||||
href={l.href}
|
|
||||||
className={cn('px-3 py-1.5 rounded-md text-sm transition-colors', textColor)}
|
|
||||||
>{l.label}</a>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Light / dark toggle */}
|
|
||||||
<button
|
|
||||||
onClick={onToggle}
|
|
||||||
className={cn(
|
|
||||||
'ml-2 flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-all',
|
|
||||||
isDark
|
|
||||||
? 'border-white/20 text-white/70 hover:text-white hover:border-white/40'
|
|
||||||
: 'border-black/15 text-black/60 hover:text-black hover:border-black/30'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isDark ? '☀ Light' : '☾ Dark'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="#contact" className={cn(
|
|
||||||
'ml-1 rounded-full border px-4 py-1.5 text-xs font-medium transition-all',
|
|
||||||
isDark
|
|
||||||
? 'border-white/25 text-white hover:bg-white hover:text-black'
|
|
||||||
: 'border-black/20 text-black hover:bg-black hover:text-white'
|
|
||||||
)}>
|
|
||||||
Hire me
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile toggle */}
|
|
||||||
<Button size="icon" variant="outline"
|
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
className={cn('md:hidden', isDark ? 'border-white/20 text-white bg-transparent' : '')}
|
|
||||||
aria-expanded={open}
|
|
||||||
>
|
|
||||||
<MenuToggleIcon open={open} className="size-5" duration={300} />
|
|
||||||
</Button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Mobile menu */}
|
|
||||||
{open && typeof window !== 'undefined' && createPortal(
|
|
||||||
<div className={cn(
|
|
||||||
'fixed top-14 inset-x-0 bottom-0 z-40 flex flex-col p-6 gap-4 backdrop-blur-lg md:hidden',
|
|
||||||
isDark ? 'bg-black/90' : 'bg-white/90'
|
|
||||||
)}>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
{links.map(l => (
|
|
||||||
<a key={l.label} href={l.href}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
className={cn('px-3 py-2 rounded-md text-sm font-medium', textColor)}
|
|
||||||
>{l.label}</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-auto flex flex-col gap-2">
|
|
||||||
<button onClick={() => { onToggle(); setOpen(false) }}
|
|
||||||
className={cn('w-full rounded-full border py-2 text-sm', isDark ? 'border-white/20 text-white' : 'border-black/20 text-black')}
|
|
||||||
>{isDark ? '☀ Light mode' : '☾ Dark mode'}</button>
|
|
||||||
<a href="#contact"
|
|
||||||
className={cn('w-full rounded-full py-2 text-sm font-medium text-center', isDark ? 'bg-white text-black' : 'bg-black text-white')}
|
|
||||||
>Hire me</a>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
document.body
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Hero section ─────────────────────────────────────────────────────────────
|
// ─── Hero section ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function HeroSection({ isDark, heading, subheading, badgeText }: {
|
type HeroProps = Pick<Page['hero'], 'richText' | 'links'>
|
||||||
isDark: boolean
|
|
||||||
heading: string
|
function HeroSection({ isDark, richText, links }: HeroProps & { isDark: boolean }) {
|
||||||
subheading: string
|
|
||||||
badgeText: string
|
|
||||||
}) {
|
|
||||||
const text = isDark ? 'text-white' : 'text-black'
|
|
||||||
const muted = isDark ? 'text-white/55' : 'text-black/55'
|
const muted = isDark ? 'text-white/55' : 'text-black/55'
|
||||||
const badge = isDark ? 'bg-white/8 border-white/15 text-white/70' : 'bg-black/5 border-black/12 text-black/60'
|
|
||||||
const borderLine = isDark ? 'bg-white/10' : 'bg-black/10'
|
const borderLine = isDark ? 'bg-white/10' : 'bg-black/10'
|
||||||
const borderLineFaint = isDark ? 'bg-white/5' : 'bg-black/5'
|
const borderLineFaint = isDark ? 'bg-white/5' : 'bg-black/5'
|
||||||
|
const richTextColor = isDark ? 'text-white' : 'text-black'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative mx-auto w-full max-w-5xl">
|
<section className="relative mx-auto w-full max-w-5xl">
|
||||||
|
|
@ -298,72 +193,47 @@ function HeroSection({ isDark, heading, subheading, badgeText }: {
|
||||||
|
|
||||||
<div className="relative flex flex-col items-center justify-center gap-5 pt-32 pb-28 px-6 text-center">
|
<div className="relative flex flex-col items-center justify-center gap-5 pt-32 pb-28 px-6 text-center">
|
||||||
|
|
||||||
{/* Badge */}
|
{/* Rich text from Payload */}
|
||||||
<a
|
{richText && (
|
||||||
href="#work"
|
<RichText
|
||||||
className={cn(
|
className={cn(
|
||||||
'group mx-auto flex w-fit items-center gap-3 rounded-full border px-3 py-1 shadow-sm',
|
|
||||||
'fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards transition-all delay-500 duration-500 ease-out',
|
|
||||||
badge
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-xs">{badgeText}</span>
|
|
||||||
<span className={cn('block h-4 border-l', isDark ? 'border-white/20' : 'border-black/15')} />
|
|
||||||
<ArrowRightIcon className="size-3 duration-150 ease-out group-hover:translate-x-1" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Headline */}
|
|
||||||
<h1 className={cn(
|
|
||||||
'fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-100 duration-500 ease-out',
|
'fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-100 duration-500 ease-out',
|
||||||
'text-balance text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl',
|
'max-w-[36.5rem] text-center',
|
||||||
text
|
richTextColor
|
||||||
)}>
|
)}
|
||||||
{heading}
|
data={richText}
|
||||||
</h1>
|
enableGutter={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Subline */}
|
{/* Links from Payload */}
|
||||||
<p className={cn(
|
{Array.isArray(links) && links.length > 0 && (
|
||||||
'fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-200 duration-500 ease-out',
|
|
||||||
'mx-auto max-w-lg text-base tracking-wide md:text-lg',
|
|
||||||
muted
|
|
||||||
)}>
|
|
||||||
{subheading}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* CTAs */}
|
|
||||||
<div className="fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-300 duration-500 ease-out flex flex-wrap items-center justify-center gap-3 pt-2">
|
<div className="fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-300 duration-500 ease-out flex flex-wrap items-center justify-center gap-3 pt-2">
|
||||||
<a
|
{links.map(({ link }, i) => (
|
||||||
href="#contact"
|
<CMSLink
|
||||||
|
key={i}
|
||||||
|
{...link}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-medium transition-all',
|
'flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-medium transition-all',
|
||||||
isDark
|
i === 0
|
||||||
|
? isDark
|
||||||
? 'bg-white text-black hover:bg-white/90'
|
? 'bg-white text-black hover:bg-white/90'
|
||||||
: 'bg-black text-white hover:bg-black/85'
|
: 'bg-black text-white hover:bg-black/85'
|
||||||
|
: isDark
|
||||||
|
? 'border border-white/25 text-white hover:bg-white/8'
|
||||||
|
: 'border border-black/20 text-black hover:bg-black/5'
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
<PhoneCallIcon className="size-3.5" />
|
))}
|
||||||
Book a call
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#work"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 rounded-full border px-5 py-2.5 text-sm font-medium transition-all',
|
|
||||||
isDark
|
|
||||||
? 'border-white/25 text-white hover:bg-white/8'
|
|
||||||
: 'border-black/20 text-black hover:bg-black/5'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
View my work
|
|
||||||
<ArrowRightIcon className="size-3.5 duration-150 ease-out group-hover:translate-x-1" />
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Logo cloud (skills that i have) ─────────────────────────────────────────
|
// ─── Logo cloud ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const LOGOS = [
|
const LOGOS = [
|
||||||
{ src: 'https://storage.efferd.com/logo/nvidia-wordmark.svg', alt: 'Nvidia' },
|
{ src: 'https://storage.efferd.com/logo/nvidia-wordmark.svg', alt: 'Nvidia' },
|
||||||
|
|
@ -376,14 +246,13 @@ const LOGOS = [
|
||||||
|
|
||||||
function LogosSection({ isDark }: { isDark: boolean }) {
|
function LogosSection({ isDark }: { isDark: boolean }) {
|
||||||
const text = isDark ? 'text-white/40' : 'text-black/40'
|
const text = isDark ? 'text-white/40' : 'text-black/40'
|
||||||
const bold = isDark ? 'text-white/70' : 'text-black/70'
|
|
||||||
const border = isDark ? 'border-white/10' : 'border-black/10'
|
const border = isDark ? 'border-white/10' : 'border-black/10'
|
||||||
const logoFilter = isDark ? 'brightness-0 invert opacity-30' : 'brightness-0 opacity-25'
|
const logoFilter = isDark ? 'brightness-0 invert opacity-30' : 'brightness-0 opacity-25'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={cn('relative border-t py-8 px-6', border)}>
|
<section className={cn('relative border-t py-8 px-6', border)}>
|
||||||
<p className={cn('mb-6 text-center text-xs tracking-widest uppercase', text)}>
|
<p className={cn('mb-6 text-center text-xs tracking-widest uppercase', text)}>
|
||||||
Skills that i have
|
Skills
|
||||||
</p>
|
</p>
|
||||||
<div className="mx-auto flex max-w-3xl flex-wrap items-center justify-center gap-8">
|
<div className="mx-auto flex max-w-3xl flex-wrap items-center justify-center gap-8">
|
||||||
{LOGOS.map(l => (
|
{LOGOS.map(l => (
|
||||||
|
|
@ -396,14 +265,12 @@ function LogosSection({ isDark }: { isDark: boolean }) {
|
||||||
|
|
||||||
// ─── Main export ──────────────────────────────────────────────────────────────
|
// ─── Main export ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// REPLACE WITH:
|
|
||||||
interface HeroPageProps {
|
interface HeroPageProps {
|
||||||
heading: string
|
richText?: Page['hero']['richText']
|
||||||
subheading: string
|
links?: Page['hero']['links']
|
||||||
badgeText: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HeroPage({ heading, subheading, badgeText }: HeroPageProps) {
|
export default function HeroPage({ richText, links }: HeroPageProps) {
|
||||||
const { isDark, toggle } = useThemeMode()
|
const { isDark, toggle } = useThemeMode()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -416,9 +283,8 @@ export default function HeroPage({ heading, subheading, badgeText }: HeroPagePro
|
||||||
|
|
||||||
{/* Page content */}
|
{/* Page content */}
|
||||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||||
<Header isDark={isDark} onToggle={toggle} />
|
|
||||||
<main>
|
<main>
|
||||||
<HeroSection isDark={isDark} heading={heading} subheading={subheading} badgeText={badgeText} />
|
<HeroSection isDark={isDark} richText={richText} links={links} />
|
||||||
<LogosSection isDark={isDark} />
|
<LogosSection isDark={isDark} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue