diff --git a/components/AboutSection.tsx b/components/AboutSection.tsx index 4a3a4a0..2ea8993 100644 --- a/components/AboutSection.tsx +++ b/components/AboutSection.tsx @@ -1,3 +1,40 @@ +import ImageCarousel, { CarouselCard } from "./ImageCarousel"; + +/** + * Static card data + * TODO: replace placeholderClass with a real images + */ +const CAROUSEL_CARDS: CarouselCard[] = [ + { + title: "Shader Programming Workshop", + caption: "Fall 2025 Workshop", + fallbackClass: "bg-card-background", + imageUrl: "/shader_programming_workshop.webp" + }, + { + title: "Ray-Marching Deep Dive", + caption: "Spring 2025 Workshop", + fallbackClass: "bg-accent/20", + imageUrl: "/ray_marching_deepdive.webp" + }, + { + title: "Monthly Coding Challenge", + caption: "October 2025", + fallbackClass: "bg-card-border/40", + }, + { + title: "Industry Speaker: Real-Time Rendering", + caption: "Fall 2025 Speaker Event", + fallbackClass: "bg-accent/10", + }, + { + title: "End-of-Semester Social", + caption: "December 2025", + fallbackClass: "bg-card-background/80", + imageUrl: "/end_of_semester_social.webp" + }, +]; + export default function AboutSection() { return (
@@ -49,8 +86,13 @@ function AboutContent() { function AboutCarousel() { return ( -
- Carousel goes here +
+ {/* + autoPlay={false} -> manual navigation (default) + autoPlay={true} -> automatic sliding every 4 s + Switch the prop to compare both behaviours + */} +
); } diff --git a/components/ImageCarousel.tsx b/components/ImageCarousel.tsx new file mode 100644 index 0000000..ccae58a --- /dev/null +++ b/components/ImageCarousel.tsx @@ -0,0 +1,284 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef } from "react"; +import { AnimatePresence, motion } from "motion/react"; +import Image from "next/image"; + +export interface CarouselCard { + title: string; + caption: string; + /* Tailwind bg-* class for the placeholder (e.g. "bg-card-background") */ + fallbackClass: string; + imageUrl?: string; +} + +interface ImageCarouselProps { + cards: CarouselCard[]; + /** + * When true the carousel advances automatically every `intervalMs` ms + * Defaults to false (manual navigation only) + */ + autoPlay?: boolean; + /** Interval in ms between automatic slides. Only used when autoPlay is true */ + intervalMs?: number; + /** + * Number of cards visible at once in the track. + * Defaults to 1. + */ + visibleCount?: number; +} + +/** + * Motion variants for the carousel. + * Defined as functions of the `custom` prop (direction) to ensure that exiting + * cards use the current direction, preventing them from sliding out the wrong way + * if direction changes mid-transition. + * + * Asymmetrical transition: + * - Entering cards sweep in quickly and cover full distance + * - Exiting cards fade and barely move (30% distance), creating a layered effect + */ +const carouselVariants = { + enter: (direction: number) => ({ + x: direction > 0 ? "100%" : "-100%", + opacity: 0, + scale: 0.9, + zIndex: 1, + }) as const, + center: { + x: 0, + opacity: 1, + scale: 1, + zIndex: 1, + transition: { + duration: 0.6, + ease: [0.16, 1, 0.3, 1], // Smooth, Apple-like ease-out + } as const, + }, + exit: (direction: number) => ({ + x: direction > 0 ? "-30%" : "30%", + opacity: 0, + scale: 0.95, + zIndex: 0, + transition: { + duration: 0.4, + ease: [0.16, 1, 0.3, 1], + }, + }) as const, +}; + +export default function ImageCarousel({ + cards, + autoPlay = false, + intervalMs = 4000, + visibleCount = 1, +}: ImageCarouselProps) { + const [currentIndex, setCurrentIndex] = useState(0); + // +1 = moving right (next), -1 = moving left (prev) + const [direction, setDirection] = useState<1 | -1>(1); + const intervalRef = useRef | null>(null); + + const goTo = useCallback( + (index: number, dir: 1 | -1) => { + setDirection(dir); + setCurrentIndex((index + cards.length) % cards.length); + }, + [cards.length] + ); + + const prev = useCallback(() => { + goTo(currentIndex - 1, -1); + }, [currentIndex, goTo]); + + const next = useCallback(() => { + goTo(currentIndex + 1, 1); + }, [currentIndex, goTo]); + + /* Auto-play */ + + const startAutoPlay = useCallback(() => { + if (!autoPlay) return; + intervalRef.current = setInterval(() => { + setDirection(1); + setCurrentIndex((prev) => (prev + 1) % cards.length); + }, intervalMs); + }, [autoPlay, cards.length, intervalMs]); + + const stopAutoPlay = useCallback(() => { + if (intervalRef.current) clearInterval(intervalRef.current); + }, []); + + useEffect(() => { + startAutoPlay(); + return stopAutoPlay; + }, [startAutoPlay, stopAutoPlay]); + + if (cards.length === 0) return null; + + // Build the window of visible cards, wrapping around the end of the array. + const visibleCards = Array.from({ length: visibleCount }, (_, offset) => + cards[(currentIndex + offset) % cards.length] + ); + + return ( +
+ {/* Card track */} +
+ {/* Animated window - slides as a single unit */} + + + {visibleCards.map((card, offset) => ( +
+ {/* Background image layer */} + {/* Next.js has an Image component which is faster than img, but it is very annoying about requiring a url for src=*/} + + {/* Text overlay */} +
+

+ {card.title} +

+

{card.caption}

+
+
+ ))} +
+
+
+ + {/* Navigation bar: left arrow, dot indicators, right arrow */} +
+ {/* Left arrow */} + + + {/* Dot indicators */} +
+ {cards.map((_, i) => ( +
+ + {/* Right arrow */} + +
+
+ ); +} + +function CardImage({ incomingCard }: { incomingCard: CarouselCard }) { + try { + return insertCard(incomingCard); + } catch (error: unknown) { + if (error instanceof TypeError) { + console.error("Image is screaming crying for help in ImageCarousel.tsx:", error.message); + } else { + console.error("Unknown error in ImageCarousel.tsx:\n\t", error); + } + return null; + } +} + +function insertCard(card: CarouselCard) { + return ( + card.imageUrl && ( + {card.title} + ) + ); +} + +/** Icon helpers (inline SVG, no icon-lib dependency) */ + +function ChevronLeft() { + return ( + + ); +} + +function ChevronRight() { + return ( + + ); +} diff --git a/public/end_of_semester_social.webp b/public/end_of_semester_social.webp new file mode 100644 index 0000000..d44924b Binary files /dev/null and b/public/end_of_semester_social.webp differ diff --git a/public/ray_marching_deepdive.webp b/public/ray_marching_deepdive.webp new file mode 100644 index 0000000..596dd3e Binary files /dev/null and b/public/ray_marching_deepdive.webp differ diff --git a/public/shader_programming_workshop.webp b/public/shader_programming_workshop.webp new file mode 100644 index 0000000..1b1913a Binary files /dev/null and b/public/shader_programming_workshop.webp differ