feat: harden portfolio and add SEO foundations
This commit is contained in:
1 parent
b079afdd73
commit
62bcfbd8fa
121 files changed
+7769
-10469
No files matched your search
@@ -1,100 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
motion,
|
||||
useAnimationFrame,
|
||||
useMotionValue,
|
||||
useScroll,
|
||||
useSpring,
|
||||
useTransform,
|
||||
} from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
|
||||
interface ScrollVelocityTextProps {
|
||||
text: string;
|
||||
baseVelocity?: number;
|
||||
direction: "left" | "right";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function wrap(min: number, max: number, v: number) {
|
||||
const rangeSize = max - min;
|
||||
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
|
||||
}
|
||||
const GROUPS = [0, 1] as const;
|
||||
|
||||
export default function ScrollVelocityText({
|
||||
text,
|
||||
baseVelocity = 2,
|
||||
direction,
|
||||
className = "",
|
||||
}: ScrollVelocityTextProps) {
|
||||
const baseX = useMotionValue(0);
|
||||
const { scrollY } = useScroll();
|
||||
const scrollVelocity = useMotionValue(0);
|
||||
const smoothVelocity = useSpring(scrollVelocity, {
|
||||
damping: 50,
|
||||
stiffness: 300,
|
||||
});
|
||||
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
|
||||
clamp: false,
|
||||
});
|
||||
|
||||
const [repetitions, setRepetitions] = useState(4);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const trackId = useId();
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [shouldReduceMotion, setShouldReduceMotion] = useState(
|
||||
() =>
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setRepetitions(Math.ceil(window.innerWidth / 400) + 2);
|
||||
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const updatePreference = (event: MediaQueryListEvent) => {
|
||||
setShouldReduceMotion(event.matches);
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
media.addEventListener("change", updatePreference);
|
||||
return () => media.removeEventListener("change", updatePreference);
|
||||
}, []);
|
||||
|
||||
// Track scroll velocity
|
||||
const prevScrollY = useRef(0);
|
||||
useEffect(() => {
|
||||
return scrollY.on("change", (latest) => {
|
||||
const delta = latest - prevScrollY.current;
|
||||
prevScrollY.current = latest;
|
||||
scrollVelocity.set(Math.abs(delta));
|
||||
});
|
||||
}, [scrollY, scrollVelocity]);
|
||||
|
||||
const directionFactor = useRef(1);
|
||||
|
||||
const x = useTransform(baseX, (v) => `${wrap(-25, 0, v)}%`);
|
||||
|
||||
useAnimationFrame((_, delta) => {
|
||||
let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
|
||||
|
||||
const vf = velocityFactor.get();
|
||||
if (vf < 0) {
|
||||
directionFactor.current = -1;
|
||||
} else if (vf > 0) {
|
||||
directionFactor.current = 1;
|
||||
}
|
||||
|
||||
moveBy += directionFactor.current * moveBy * vf;
|
||||
baseX.set(baseX.get() + moveBy);
|
||||
});
|
||||
const animationPaused = shouldReduceMotion || isPaused;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`overflow-hidden whitespace-nowrap ${className}`}
|
||||
>
|
||||
<motion.div className="inline-flex" style={{ x }}>
|
||||
{[...Array(repetitions)].map((_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="mr-8 inline-block font-display text-[clamp(3rem,10vw,8rem)] font-extrabold uppercase leading-none tracking-[-0.04em]"
|
||||
>
|
||||
{text}
|
||||
<span className="mx-8 inline-block text-[var(--slash)]">
|
||||
•
|
||||
<div className={`scroll-velocity-text ${className}`.trim()}>
|
||||
<div className="scroll-velocity-viewport">
|
||||
<div
|
||||
id={trackId}
|
||||
className={`scroll-velocity-track ${
|
||||
direction === "left" ? "animate-marquee" : "animate-marquee-reverse"
|
||||
}`}
|
||||
style={{
|
||||
animationPlayState: animationPaused ? "paused" : undefined,
|
||||
}}
|
||||
>
|
||||
{GROUPS.map((group) => (
|
||||
<span
|
||||
key={group}
|
||||
className="scroll-velocity-group"
|
||||
aria-hidden={group === 0 ? undefined : "true"}
|
||||
>
|
||||
<span className="scroll-velocity-item">
|
||||
<span aria-hidden={group === 0 ? "false" : "true"}>{text}</span>
|
||||
<span aria-hidden="true" className="scroll-velocity-dot">
|
||||
•
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!shouldReduceMotion ? (
|
||||
<button
|
||||
type="button"
|
||||
className="scroll-velocity-control"
|
||||
aria-controls={trackId}
|
||||
aria-pressed={isPaused}
|
||||
onClick={() => setIsPaused((paused) => !paused)}
|
||||
>
|
||||
{isPaused ? "Retomar animação" : "Pausar animação"}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user