portfolio anim
This commit is contained in:
@@ -1,23 +1,65 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* nameStage:
|
||||||
|
* 0 – all hidden, b/vic.com reset to off-screen (transition: none)
|
||||||
|
* 1 – "Alex" fades in, b/vic.com still off-screen
|
||||||
|
* 2 – b slides in from left, vic.com from right; "A" → "a"
|
||||||
|
* 3 – b/vic.com fade out in-place; "a" → "A"
|
||||||
|
*
|
||||||
|
* Cycle: 0→1 (300ms) →2 (1600ms) →3 (4600ms) →0 (7400ms) → repeat every 7600ms
|
||||||
|
* Content (subtitle, buttons, etc.) fades in once at 3000ms and stays.
|
||||||
|
*/
|
||||||
|
|
||||||
export function HeroSection() {
|
export function HeroSection() {
|
||||||
const [stage, setStage] = useState(0)
|
const [nameStage, setNameStage] = useState(0)
|
||||||
|
const [contentVisible, setContentVisible] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t1 = setTimeout(() => setStage(1), 300)
|
const timers: ReturnType<typeof setTimeout>[] = []
|
||||||
const t2 = setTimeout(() => setStage(2), 1600)
|
let alive = true
|
||||||
const t3 = setTimeout(() => setStage(3), 3000)
|
const add = (fn: () => void, ms: number) =>
|
||||||
return () => {
|
timers.push(setTimeout(() => { if (alive) fn() }, ms))
|
||||||
clearTimeout(t1)
|
|
||||||
clearTimeout(t2)
|
const CYCLE = 7600
|
||||||
clearTimeout(t3)
|
|
||||||
|
const scheduleCycle = (offset: number) => {
|
||||||
|
add(() => setNameStage(1), offset + 300)
|
||||||
|
add(() => setNameStage(2), offset + 1600)
|
||||||
|
add(() => setNameStage(3), offset + 4600)
|
||||||
|
add(() => setNameStage(0), offset + 7400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleCycle(0)
|
||||||
|
add(() => setContentVisible(true), 3000)
|
||||||
|
|
||||||
|
for (let i = 1; i <= 40; i++) scheduleCycle(CYCLE * i)
|
||||||
|
|
||||||
|
return () => { alive = false; timers.forEach(clearTimeout) }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fadeUp = (active: boolean) => ({
|
/** Styles for the sliding "b" and "vic.com" spans */
|
||||||
|
const sideStyle = (side: 'left' | 'right'): React.CSSProperties => {
|
||||||
|
const offscreen = side === 'left' ? 'translateX(-110vw)' : 'translateX(110vw)'
|
||||||
|
const reset = nameStage === 0
|
||||||
|
const visible = nameStage === 2
|
||||||
|
const fadingOut = nameStage === 3
|
||||||
|
return {
|
||||||
|
display: 'inline-block',
|
||||||
|
color: '#22d3ee',
|
||||||
|
opacity: visible ? 1 : 0,
|
||||||
|
transform: visible || fadingOut ? 'translateX(0)' : offscreen,
|
||||||
|
transition: reset
|
||||||
|
? 'none'
|
||||||
|
: 'opacity 0.7s ease-out, transform 1s ease-out',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generic fade-up for content blocks */
|
||||||
|
const fadeUp = (active: boolean, delay = '0s'): React.CSSProperties => ({
|
||||||
opacity: active ? 1 : 0,
|
opacity: active ? 1 : 0,
|
||||||
transform: active ? 'translateY(0)' : 'translateY(16px)',
|
transform: active ? 'translateY(0)' : 'translateY(18px)',
|
||||||
transition: 'opacity 0.8s ease, transform 0.8s ease',
|
transition: `opacity 0.8s ease ${delay}, transform 0.8s ease ${delay}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,64 +81,54 @@ export function HeroSection() {
|
|||||||
<div className="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 rounded-full bg-accent/5 blur-3xl pointer-events-none" />
|
<div className="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 rounded-full bg-accent/5 blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
<div className="relative z-10 max-w-4xl mx-auto px-6 py-24 text-center">
|
<div className="relative z-10 max-w-4xl mx-auto px-6 py-24 text-center">
|
||||||
{/* Location badge — Stage 3 */}
|
|
||||||
<div style={fadeUp(stage >= 3)}>
|
{/* Location badge */}
|
||||||
|
<div style={fadeUp(contentVisible)}>
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 mb-8 rounded-full border border-zinc-800 bg-zinc-900/60 text-xs text-zinc-400 font-mono tracking-wider">
|
<div className="inline-flex items-center gap-2 px-3 py-1.5 mb-8 rounded-full border border-zinc-800 bg-zinc-900/60 text-xs text-zinc-400 font-mono tracking-wider">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
||||||
Switzerland, Bern
|
Switzerland, Bern
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name — animated across 3 stages */}
|
{/* ── Animated heading ── */}
|
||||||
<h1 className="font-heading font-extrabold text-6xl md:text-8xl tracking-tight mb-4 leading-none">
|
<h1
|
||||||
{/* "b" slides in from the left — Stage 2 */}
|
className="font-heading font-extrabold mb-4 leading-none"
|
||||||
<span
|
style={{
|
||||||
style={{
|
fontSize: 'clamp(3rem, 9.5vw, 7.5rem)',
|
||||||
display: 'inline-block',
|
letterSpacing: '-0.02em',
|
||||||
color: '#22d3ee',
|
whiteSpace: 'nowrap',
|
||||||
opacity: stage >= 2 ? 1 : 0,
|
transform: 'scaleX(0.84)',
|
||||||
transform: stage >= 2 ? 'translateX(0)' : 'translateX(-160px)',
|
transformOrigin: 'center center',
|
||||||
transition: 'opacity 1s ease, transform 1s ease',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{/* "b" — slides in from the left */}
|
||||||
b
|
<span style={sideStyle('left')}>b</span>
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* "Alex" fades in centered — Stage 1 */}
|
{/* "Alex" / "alex" — fades in first */}
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
opacity: stage >= 1 ? 1 : 0,
|
opacity: nameStage >= 1 ? 1 : 0,
|
||||||
transition: 'opacity 0.8s ease',
|
transition: nameStage === 0 ? 'none' : 'opacity 0.8s ease-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Alex
|
{nameStage === 2 ? 'alex' : 'Alex'}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* "vic.com" slides in from the right — Stage 2 */}
|
{/* "vic.com" — slides in from the right */}
|
||||||
<span
|
<span style={sideStyle('right')}>vic.com</span>
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
color: '#22d3ee',
|
|
||||||
opacity: stage >= 2 ? 1 : 0,
|
|
||||||
transform: stage >= 2 ? 'translateX(0)' : 'translateX(160px)',
|
|
||||||
transition: 'opacity 1s ease, transform 1s ease',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
vic.com
|
|
||||||
</span>
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Role — Stage 3 */}
|
{/* Role */}
|
||||||
<div style={fadeUp(stage >= 3)}>
|
<div style={fadeUp(contentVisible, '0s')}>
|
||||||
<p className="font-heading font-medium text-2xl md:text-3xl text-gradient mb-6 tracking-tight">
|
<p className="font-heading font-medium text-2xl md:text-3xl text-gradient mb-6 tracking-tight">
|
||||||
Java / Fullstack Developer
|
Java / Fullstack Developer
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description — Stage 3 */}
|
{/* Description */}
|
||||||
<div style={{ ...fadeUp(stage >= 3), transitionDelay: '0.1s' }}>
|
<div style={fadeUp(contentVisible, '0.1s')}>
|
||||||
<p className="text-zinc-400 text-lg max-w-xl mx-auto mb-10 font-body leading-relaxed">
|
<p className="text-zinc-400 text-lg max-w-xl mx-auto mb-10 font-body leading-relaxed">
|
||||||
Building backend systems and intelligent applications with{' '}
|
Building backend systems and intelligent applications with{' '}
|
||||||
<span className="text-zinc-200 font-mono text-base">Spring Boot</span>,{' '}
|
<span className="text-zinc-200 font-mono text-base">Spring Boot</span>,{' '}
|
||||||
@@ -105,8 +137,8 @@ export function HeroSection() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CTA buttons — Stage 3 */}
|
{/* CTA buttons */}
|
||||||
<div style={{ ...fadeUp(stage >= 3), transitionDelay: '0.2s' }}>
|
<div style={fadeUp(contentVisible, '0.2s')}>
|
||||||
<div className="flex flex-wrap items-center justify-center gap-4">
|
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||||
<a
|
<a
|
||||||
href="/static/cv.pdf"
|
href="/static/cv.pdf"
|
||||||
@@ -150,30 +182,17 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scroll hint — Stage 3 */}
|
{/* Scroll hint */}
|
||||||
<div style={{ ...fadeUp(stage >= 3), transitionDelay: '0.3s' }}>
|
<div style={fadeUp(contentVisible, '0.3s')}>
|
||||||
<div className="mt-16 flex flex-col items-center gap-2 text-zinc-600 text-xs font-mono tracking-widest">
|
<div className="mt-16 flex flex-col items-center gap-2 text-zinc-600 text-xs font-mono tracking-widest">
|
||||||
<span>SCROLL</span>
|
<span>SCROLL</span>
|
||||||
<svg
|
<svg width="14" height="20" viewBox="0 0 14 20" fill="none" className="animate-bounce">
|
||||||
width="14"
|
<rect x="1" y="1" width="12" height="18" rx="6" stroke="currentColor" strokeWidth="1.5" />
|
||||||
height="20"
|
|
||||||
viewBox="0 0 14 20"
|
|
||||||
fill="none"
|
|
||||||
className="animate-bounce"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
x="1"
|
|
||||||
y="1"
|
|
||||||
width="12"
|
|
||||||
height="18"
|
|
||||||
rx="6"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<circle cx="7" cy="6" r="1.5" fill="currentColor" className="animate-pulse" />
|
<circle cx="7" cy="6" r="1.5" fill="currentColor" className="animate-pulse" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user