Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions app/projects/eternalcore/eternal-showcase.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client";

import { AnimatePresence, motion } from "framer-motion";
import { AnimatePresence, motion, type Transition } from "framer-motion";
import { Maximize2, X } from "lucide-react";
import Image from "next/image";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { FadeIn } from "@/components/ui/motion/motion-components";
import { useReducedMotion } from "@/hooks/use-reduced-motion";

const marqueeItems = [
// Row 1: Essentials & Chat
Expand Down Expand Up @@ -117,29 +118,38 @@ const InfiniteMarquee = ({
items,
direction = "left",
speed = 40,
prefersReducedMotion,
onItemClick,
}: {
items: typeof marqueeItems;
direction?: "left" | "right";
speed?: number;
prefersReducedMotion: boolean;
onItemClick: (item: (typeof marqueeItems)[0]) => void;
}) => {
const marqueeAnimation = prefersReducedMotion
? undefined
: {
x: direction === "left" ? ["0%", "-50%"] : ["-50%", "0%"],
};
const marqueeTransition: Transition | undefined = prefersReducedMotion
? undefined
: {
duration: speed,
ease: "linear",
repeat: Number.POSITIVE_INFINITY,
repeatType: "loop",
};

return (
<div className="relative flex w-full overflow-hidden">
<motion.div
animate={{
x: direction === "left" ? ["0%", "-50%"] : ["-50%", "0%"],
}}
animate={marqueeAnimation}
className="flex gap-4 py-4"
style={{
width: "max-content",
}}
transition={{
duration: speed,
ease: "linear",
repeat: Number.POSITIVE_INFINITY,
repeatType: "loop",
}}
transition={marqueeTransition}
>
{[...items, ...items].map((item, i) => (
<motion.div
Expand Down Expand Up @@ -182,6 +192,7 @@ const InfiniteMarquee = ({
export function EternalShowcase() {
const [selectedItem, setSelectedItem] = useState<(typeof marqueeItems)[0] | null>(null);
const midPoint = Math.ceil(marqueeItems.length / 2);
const prefersReducedMotion = useReducedMotion();

return (
<>
Expand All @@ -203,12 +214,14 @@ export function EternalShowcase() {
direction="left"
items={marqueeItems.slice(0, midPoint)}
onItemClick={setSelectedItem}
prefersReducedMotion={prefersReducedMotion}
speed={50}
/>
<InfiniteMarquee
direction="right"
items={marqueeItems.slice(midPoint)}
onItemClick={setSelectedItem}
prefersReducedMotion={prefersReducedMotion}
speed={50}
/>
</FadeIn>
Expand Down
55 changes: 50 additions & 5 deletions app/projects/eternalcore/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
"use client";

import { m, motion, useScroll, useTransform } from "framer-motion";
import { gsap } from "gsap";
import { Book, Check, ChevronRight, Download, Layers, Settings, Zap } from "lucide-react";
import Image from "next/image";
import { useRef } from "react";
import { useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { FacadePattern } from "@/components/ui/facade-pattern";
import { FadeIn, MotionSection, ScaleIn, SlideIn } from "@/components/ui/motion/motion-components";
import { slideUp } from "@/lib/animations/variants";
import { useReducedMotion } from "@/hooks/use-reduced-motion";

import { ConfigPreview } from "./config-preview";
import { EternalShowcase } from "./eternal-showcase";

export default function EternalCorePage() {
const pageRef = useRef<HTMLDivElement>(null);
const targetRef = useRef(null);
const prefersReducedMotion = useReducedMotion();
const { scrollYProgress } = useScroll({
target: targetRef,
offset: ["start start", "end start"],
Expand All @@ -23,13 +27,54 @@ export default function EternalCorePage() {
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
const scale = useTransform(scrollYProgress, [0, 0.5], [1, 0.95]);

useEffect(() => {
if (prefersReducedMotion) {
return;
}

const root = pageRef.current;
if (!root) {
return;
}

const ctx = gsap.context(() => {
const q = gsap.utils.selector(root);
const blobs = q<HTMLElement>("[data-ambient-blob]");

blobs.forEach((blob, index) => {
gsap.to(blob, {
x: index % 2 === 0 ? 24 : -24,
y: index % 2 === 0 ? -32 : 32,
duration: 14 + index * 2,
repeat: -1,
yoyo: true,
ease: "sine.inOut",
});
});
}, root);

return () => ctx.revert();
}, [prefersReducedMotion]);

return (
<div className="relative min-h-screen bg-gray-50 text-gray-900 selection:bg-[#9d6eef]/30 dark:bg-gray-950 dark:text-white">
<div
className="relative min-h-screen bg-gray-50 text-gray-900 selection:bg-[#9d6eef]/30 dark:bg-gray-950 dark:text-white"
ref={pageRef}
>
{/* Background Decor */}
<div className="pointer-events-none absolute inset-0 z-0 overflow-hidden">
<div className="absolute top-0 right-0 h-[600px] w-[600px] translate-x-1/3 -translate-y-1/4 rounded-full bg-[#9d6eef]/20 blur-3xl filter dark:bg-[#9d6eef]/10" />
<div className="absolute top-[40%] left-0 h-[600px] w-[600px] -translate-x-1/3 rounded-full bg-[#d946ef]/20 mix-blend-multiply blur-3xl filter dark:bg-[#d946ef]/10 dark:mix-blend-screen" />
<div className="absolute right-0 bottom-20 h-[600px] w-[600px] translate-x-1/3 rounded-full bg-[#8b5cf6]/20 mix-blend-multiply blur-3xl filter dark:bg-[#8b5cf6]/10 dark:mix-blend-screen" />
<div
className="absolute top-0 right-0 h-[600px] w-[600px] translate-x-1/3 -translate-y-1/4 rounded-full bg-[#9d6eef]/20 blur-3xl filter dark:bg-[#9d6eef]/10"
data-ambient-blob
/>
<div
className="absolute top-[40%] left-0 h-[600px] w-[600px] -translate-x-1/3 rounded-full bg-[#d946ef]/20 mix-blend-multiply blur-3xl filter dark:bg-[#d946ef]/10 dark:mix-blend-screen"
data-ambient-blob
/>
<div
className="absolute right-0 bottom-20 h-[600px] w-[600px] translate-x-1/3 rounded-full bg-[#8b5cf6]/20 mix-blend-multiply blur-3xl filter dark:bg-[#8b5cf6]/10 dark:mix-blend-screen"
data-ambient-blob
/>
<FacadePattern className="absolute inset-0 h-full opacity-30 dark:opacity-10" />
</div>

Expand Down
7 changes: 7 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 51 additions & 22 deletions components/home/projects/projects-section.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"use client";

import { motion } from "framer-motion";
import { gsap } from "gsap";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { useAutoScroll } from "@/hooks/use-auto-scroll";
import { useReducedMotion } from "@/hooks/use-reduced-motion";

interface Project {
name: string;
Expand Down Expand Up @@ -52,44 +55,69 @@ const projects: Project[] = [
];

export default function Projects() {
const sectionRef = useRef<HTMLElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const prefersReducedMotion = useReducedMotion();

useAutoScroll(scrollRef, {
speed: 36,
isPaused: isHovered,
prefersReducedMotion,
maxFrameRate: 30,
});

// Auto-scroll logic
useEffect(() => {
const scrollContainer = scrollRef.current;
if (!scrollContainer) {
if (prefersReducedMotion) {
return;
}

const section = sectionRef.current;
if (!section) {
return;
}

let animationFrameId: number;
let scrollPos = scrollContainer.scrollLeft;
const speed = 0.6; // Slightly slower for better viewing of images
const ctx = gsap.context(() => {
const q = gsap.utils.selector(section);
const cards = q<HTMLElement>("[data-project-card]");
if (!cards.length) {
return;
}

const scroll = () => {
if (!isHovered && scrollContainer) {
scrollPos += speed;
gsap.set(cards, { autoAlpha: 0, y: 24 });

if (scrollPos >= scrollContainer.scrollWidth / 3) {
scrollPos = 0;
}
const observer = new IntersectionObserver(
([entry]) => {
if (!entry?.isIntersecting) {
return;
}

scrollContainer.scrollLeft = scrollPos;
} else if (isHovered && scrollContainer) {
scrollPos = scrollContainer.scrollLeft;
}
animationFrameId = requestAnimationFrame(scroll);
};
gsap.to(cards, {
autoAlpha: 1,
y: 0,
duration: 0.7,
ease: "power3.out",
stagger: 0.08,
clearProps: "opacity,transform",
});

observer.disconnect();
},
{ threshold: 0.3 }
);

observer.observe(section);

animationFrameId = requestAnimationFrame(scroll);
return () => observer.disconnect();
}, section);

return () => cancelAnimationFrame(animationFrameId);
}, [isHovered]);
return () => ctx.revert();
}, [prefersReducedMotion]);

const displayProjects = [...projects, ...projects, ...projects];

return (
<section className="relative w-full py-24" id="projects">
<section className="relative w-full py-24" id="projects" ref={sectionRef}>
{/* Main Container - Keeps everything aligned with navbar */}
<div className="mx-auto max-w-[90rem] px-4 md:px-8">
{/* Header content */}
Expand Down Expand Up @@ -155,6 +183,7 @@ function ProjectCard({ project }: { project: Project }) {
return (
<Link
className="group relative block aspect-[16/9] w-[300px] shrink-0 transform-gpu overflow-hidden rounded-2xl border border-gray-200 bg-gray-100 shadow-lg transition-all duration-500 will-change-transform hover:shadow-2xl md:w-[420px] dark:border-gray-800 dark:bg-gray-900"
data-project-card
href={project.url}
>
<Image
Expand Down
68 changes: 68 additions & 0 deletions hooks/use-auto-scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import { useEffect, useRef } from "react";

interface AutoScrollOptions {
speed: number;
isPaused?: boolean;
prefersReducedMotion?: boolean;
maxFrameRate?: number;
}

export function useAutoScroll(
ref: React.RefObject<HTMLElement | null>,
{ speed, isPaused = false, prefersReducedMotion = false, maxFrameRate = 30 }: AutoScrollOptions
) {
const rafIdRef = useRef<number | null>(null);
const lastTimeRef = useRef<number | null>(null);
const scrollPosRef = useRef(0);

useEffect(() => {
const element = ref.current;
if (!element) {
return;
}

if (isPaused || prefersReducedMotion) {
scrollPosRef.current = element.scrollLeft;
return;
}

const minFrameMs = 1000 / Math.max(1, maxFrameRate);

const tick = (time: number) => {
if (!element) {
return;
}

if (lastTimeRef.current === null) {
lastTimeRef.current = time;
}

const delta = time - lastTimeRef.current;
if (delta >= minFrameMs) {
scrollPosRef.current += (speed * delta) / 1000;
const maxScroll = element.scrollWidth / 3;

if (scrollPosRef.current >= maxScroll) {
scrollPosRef.current = 0;
}

element.scrollLeft = scrollPosRef.current;
lastTimeRef.current = time;
}

rafIdRef.current = requestAnimationFrame(tick);
};

rafIdRef.current = requestAnimationFrame(tick);

return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
}
rafIdRef.current = null;
lastTimeRef.current = null;
};
}, [isPaused, maxFrameRate, prefersReducedMotion, ref, speed]);
}
Loading