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.
Featured Projects Component
Props Interface
interface ProjectsProps {
onSetExperienceSection: () => void;
}
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...
];
Featured Projects
Four featured projects are displayed in the sticky-scroll view:
-
Brainrot Master Vault - HackPrinceton 2025 Winner
- AI-curated podcast episodes from short-form videos
- Best Self-Hosted Inference award
-
rateourclub.com - Community platform
- Student reviews for 100+ college organizations
- Similar to Rate My Professor
-
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
-
Mine Alliance - Principled Innovation Hackathon Winner
- Mining community platform for Arizona
- Real-time data and environmental insights
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;
};
Callback to close expanded view and return to featured projects
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
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>
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 2-column grid for medium screens Single column for small screens
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"
}