Skip to main content
The Projects component provides two distinct views: a featured projects showcase with sticky-scroll behavior and an expanded grid view showing all projects.

Overview

Location: src/app/components/sections/Projects.tsx (Featured view) Location: src/app/components/sections/OpenedProjects.tsx (Expanded view) The projects section uses a sticky-scroll reveal effect to showcase featured projects with images that remain fixed while descriptions scroll. Users can expand to view all projects in a comprehensive grid layout.

Props Interface

interface ProjectsProps {
  onSetExperienceSection: () => void;
}
onSetExperienceSection
() => void
required
Callback function to toggle between featured and expanded project views

Implementation

export default function Projects({ onSetExperienceSection }: ProjectsProps) {
  return (
    <div
      className="max-md:hidden max-lg:col-span-1 max-lg:row-span-1 col-span-2 row-span-6 col-start-5 bg-spotify-light-dark rounded-xl overflow-hidden"
      id="projects"
    >
      <div className="p-4">
        <div className="flex gap-3 justify-center">
          {/* Primary Button (Green) */}
          <button className="flex items-center justify-center text-sm font-bold 
            bg-spotify-green hover:bg-spotify-dark-green hover:scale-105 
            px-5 py-2 rounded-full gap-2 transition-all duration-200
            min-w-[180px]"
            aria-label="View featured projects below"
          >
            Featured Projects
            <FaArrowDown className="text-base" />
          </button>

          {/* Secondary Button (White Border) */}
          <button
            className="flex items-center justify-center text-sm font-bold 
            border border-[#727272] hover:border-white hover:scale-105 
            text-white px-5 py-2 rounded-full gap-2 transition-all duration-200
            animate-pulse hover:animate-none hover:bg-white hover:text-black
            shadow-[0_0_15px_rgba(255,255,255,0.3)] hover:shadow-[0_0_20px_rgba(255,255,255,0.5)]
            min-w-[180px]"
            onClick={onSetExperienceSection}
            aria-label="Explore all projects in expanded view"
          >
            Explore All Projects
            <GrLinkNext className="text-base" />
          </button>
        </div>
      </div>
      <StickyScroll content={projectLists} />
    </div>
  );
}

Project Data Structure

const projectLists: {
  title: string;
  description: string;
  content?: React.ReactNode | any;
}[] = [
  {
    title: "Brainrot Master Vault (HackPrinceton 2025 Winner 🏆)",
    description:
      "BrainRot Master Vault turns short-form videos into AI-curated podcast episodes and knowledge graphs. Built at HackPrinceton 2025 and won Best Self-Hosted Inference.",
    content: (
      <a
        target="_blank"
        rel="noopener noreferrer"
        href="https://www.brainrotmastervaultovercooked.tech/"
      >
        <Image
          src={`/gallery.jpg`}
          alt="Brainrot Master Vault Website"
          width={500}
          height={500}
        />
      </a>
    ),
  },
  // Additional projects...
];
Four featured projects are displayed in the sticky-scroll view:
  1. Brainrot Master Vault - HackPrinceton 2025 Winner
    • AI-curated podcast episodes from short-form videos
    • Best Self-Hosted Inference award
  2. rateourclub.com - Community platform
    • Student reviews for 100+ college organizations
    • Similar to Rate My Professor
  3. Sip n Play Café Website - Codédex Hackathon Winner
    • Interactive board game café website
    • 500+ game catalog with 3D animated menu
    • Best UI/UX Design award
  4. Mine Alliance - Principled Innovation Hackathon Winner
    • Mining community platform for Arizona
    • Real-time data and environmental insights

Sticky Scroll Behavior

The StickyScroll component creates an engaging scroll effect: Location: src/app/components/ui/sticky-scroll-reveal.tsx
export const StickyScroll = ({
  content,
  contentClassName,
}: {
  content: {
    title: string;
    description: string;
    content?: React.ReactNode | any;
  }[];
  contentClassName?: string;
}) => {
  const [activeCard, setActiveCard] = React.useState(0);
  const ref = useRef<any>(null);
  const { scrollYProgress } = useScroll({
    container: ref,
    offset: ["start start", "end start"],
  });

  // Track scroll position and update active card
  useMotionValueEvent(scrollYProgress, "change", (latest) => {
    const cardsBreakpoints = content.map((_, index) => index / cardLength);
    const closestBreakpointIndex = cardsBreakpoints.reduce(
      (acc, breakpoint, index) => {
        const distance = Math.abs(latest - breakpoint);
        if (distance < Math.abs(latest - cardsBreakpoints[acc])) {
          return index;
        }
        return acc;
      },
      0
    );
    setActiveCard(closestBreakpointIndex);
  });

  return (
    <motion.div
      animate={{
        backgroundColor: backgroundColors[activeCard % backgroundColors.length],
        opacity: "95%",
      }}
      className="h-[38.5rem] overflow-y-auto flex justify-center relative space-x-4 rounded-md py-6 px-4"
      ref={ref}
    >
      {/* Scrollable descriptions and sticky image */}
    </motion.div>
  );
};
The background color animates as users scroll through projects, cycling through: #6DC5D1, #8CCDEB, #90D26D, #FDAF7B

Expanded Projects View

Overview

Location: src/app/components/sections/OpenedProjects.tsx The expanded view displays all projects in a responsive grid with expandable descriptions.

Props Interface

type ProjectsPropsWithClassName = ProjectsProps & {
  className?: string;
};
onSetExperienceSection
() => void
required
Callback to close expanded view and return to featured projects
className
string
Additional CSS classes for custom styling

Implementation

export default function OpenedExperienceItem({
  onSetExperienceSection,
  className,
}: ProjectsPropsWithClassName) {
  const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
  
  // Dynamic date formatting
  const currentDate = new Date();
  const formattedDate = currentDate.toLocaleDateString('en-US', { 
    month: 'long', 
    year: 'numeric' 
  });

  return (
    <div
      id="project"
      className={`${className} col-span-4 row-span-6 col-start-3 row-start-1 bg-spotify-light-dark rounded-xl overflow-hidden flex flex-col sm:h-[800px]`}
    >
      <div className="sticky top-0 bg-spotify-light-dark z-10">
        <div className="flex justify-between items-center px-6 py-4 bg-spotify-gray">
          <div className="flex flex-col gap-1.5">
            <h2 className="text-2xl font-bold">Projects</h2>
            <p className="flex items-center text-spotify-grey text-sm gap-1.5">
              <CiGlobe className="text-xl text-spotify-white" />
              {personalProjects.length} completed projects • Updated {formattedDate}
            </p>
          </div>

          <button
            type="button"
            className="p-3 hover:bg-[#282828] rounded-full transition-colors max-md:hidden"
            onClick={onSetExperienceSection}
            aria-label="Close expanded projects view"
          >
            <MdClose className="text-2xl" />
          </button>
        </div>
      </div>

      <section className="grid grid-cols-3 max-xl:grid-cols-2 max-md:grid-cols-1 gap-4 p-6 pt-2 overflow-y-auto">
        {personalProjects.map((project, idx) => (
          <Link
            target="_blank"
            key={project.title}
            href={project.href}
            rel="noopener noreferrer"
            className="group"
          >
            <ProjectCard
              project={project}
              idx={idx}
              isExpanded={expandedIdx === idx}
              onToggle={() => setExpandedIdx(expandedIdx === idx ? null : idx)}
            />
          </Link>
        ))}
      </section>
    </div>
  );
}

Project Card Component

function ProjectCard({
  project,
  idx,
  isExpanded,
  onToggle,
}: {
  project: SingleProjectType;
  idx: number;
  isExpanded: boolean;
  onToggle: () => void;
}) {
  return (
    <div className="relative sm:hover:bg-[#282828] transition-colors p-4 rounded-xl flex flex-col gap-3">
      <div className="relative">
        <Image
          src={project.imageSrc}
          alt={project.imageAlt}
          width={400}
          height={400}
          className="rounded-lg w-full aspect-video object-cover"
        />
        <div className="absolute inset-0 flex items-center justify-center opacity-0 sm:group-hover:opacity-100 transition-opacity bg-black/20">
          <IoPlayCircle className="text-spotify-green text-5xl drop-shadow-lg" />
        </div>
      </div>
      <div className="flex flex-col gap-2">
        <h4 className="text-lg font-bold">{project.title}</h4>
        <p
          className={`text-sm text-spotify-grey break-words whitespace-pre-line ${
            isExpanded ? "" : "line-clamp-2"
          }`}
        >
          {project.description}
        </p>
        {project.description.length > 80 && (
          <button
            className={`flex items-center gap-1 text-xs font-semibold px-2 py-1 rounded-md transition-all duration-200 w-fit
              ${
                isExpanded
                  ? "bg-spotify-green/20 text-spotify-green"
                  : "bg-[#232323] text-spotify-green hover:bg-spotify-green/10"
              }
              hover:shadow-md`}
            onClick={(e) => {
              e.preventDefault();
              onToggle();
            }}
          >
            <span>{isExpanded ? "Show less" : "Read more"}</span>
            <FiChevronDown
              className={`transition-transform duration-200 text-base ml-0.5 ${
                isExpanded ? "rotate-180" : ""
              }`}
            />
          </button>
        )}
        <div className="flex gap-1.5 flex-wrap">
          {project.tech.map((el) => (
            <div
              className="text-[10px] bg-spotify-green px-2 py-1 rounded-md"
              key={el}
            >
              {el}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Features

Expandable Descriptions

Long project descriptions are truncated to 2 lines with a “Read more” button to expand them inline.

Hover Effects

  • Play icon appears on image hover (desktop only)
  • Card background changes on hover
  • Smooth transitions for all interactions

Technology Tags

Each project displays technology badges:
<div className="flex gap-1.5 flex-wrap">
  {project.tech.map((el) => (
    <div className="text-[10px] bg-spotify-green px-2 py-1 rounded-md" key={el}>
      {el}
    </div>
  ))}
</div>

Dynamic Header

The header shows total project count and dynamically updates the date:
const currentDate = new Date();
const formattedDate = currentDate.toLocaleDateString('en-US', { 
  month: 'long', 
  year: 'numeric' 
});

<p>
  {personalProjects.length} completed projects • Updated {formattedDate}
</p>

Grid Layout

3-column grid for large screens
grid-cols-3

Data Integration

Projects are imported from a centralized data file:
import {
  personalProjects,
  type personalProjectType,
} from "../../../../data/projects";
Centralizing project data allows for easy updates without modifying component code.

Dependencies

{
  "next/image": "Image optimization",
  "next/link": "Client-side navigation",
  "react-icons": "Icons (FaArrowDown, GrLinkNext, MdClose, IoPlayCircle, FiChevronDown, CiGlobe)",
  "framer-motion": "Scroll animations",
  "../ui/sticky-scroll-reveal": "Custom sticky-scroll component"
}

Build docs developers (and LLMs) love