Files
hwilliams-dev/components/Projects.tsx
2026-05-31 23:34:21 -04:00

150 lines
4.7 KiB
TypeScript

"use client";
import { PROJECTS } from "@/data/content";
import { useStaggerReveal } from "@/hooks/useAnimations";
import React, { useState, useCallback } from 'react';
import ProjectCard from "@/components/ProjectCard";
import { Layout, Responsive, useContainerWidth, horizontalCompactor, verticalCompactor, DefaultBreakpoints } from "react-grid-layout";
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
export default function Projects() {
const rowHeight = 250;
const defaultExpandedHeightUnits = 2;
const visible = useStaggerReveal(PROJECTS.length, 100);
const [selectedProject, setSelectedProject] = useState<number>(1);
const { width, containerRef, mounted } = useContainerWidth();
const [topCellHeight, setTopCellHeight] = useState(defaultExpandedHeightUnits);
const [layouts, setLayouts] = useState<Record<DefaultBreakpoints, Layout>>(updateLayout(1));
function setExpandedHeight(height: number) {
const gridUnits = height === 0 ? defaultExpandedHeightUnits : Math.ceil(height / rowHeight);
setTopCellHeight(gridUnits);
setLayouts(updateLayout(selectedProject, gridUnits)); // pass both
}
function setSelectedHandler(projectId: number) {
const id = Number(projectId);
setSelectedProject(id);
setLayouts(updateLayout(id, topCellHeight)); // pass both
}
function updateLayout(activeId: number, cellHeight?: number) : Record<DefaultBreakpoints, Layout> {
const currentSelected = activeId ?? selectedProject;
const currentCellHeight = cellHeight ?? topCellHeight;
let isSelected = (projectId: number) => projectId === currentSelected;
return {
"sm": PROJECTS.map((project, i) => {
return {
i: project.id.toString(),
x: isSelected(project.id) ? 0 : (i % 2) + 1,
y: isSelected(project.id) ? 0 : Math.floor(i / 2) + (!isSelected(project.id) ? 1 : 0),
w: isSelected(project.id) ? 2 : 1,
h: project.images && project.images.length > 0 ? currentCellHeight : 1
};
}),
"xs": PROJECTS.map((project, i) => {
return {
i: project.id.toString(),
x: 0,
y: i,
w: 2,
h: project.images && project.images.length > 0 ? currentCellHeight : 1
};
}),
"xxs": PROJECTS.map((project, i) => {
return {
i: project.id.toString(),
x: 0,
y: i,
w: 2,
h: project.images && project.images.length > 0 ? currentCellHeight : 1
};
}),
"md": PROJECTS.map((project, i) => {
return {
i: project.id.toString(),
x: isSelected(project.id) ? 0 : (i % 2) + 1,
y: isSelected(project.id) ? 0 : Math.floor(i / 2) + (!isSelected(project.id) ? 1 : 0),
w: isSelected(project.id) ? 2 : 1,
h: project.images && project.images.length > 0 ? currentCellHeight : 1
};
}),
"lg": PROJECTS.map((project, i) => {
return {
i: project.id.toString(),
x: isSelected(project.id) ? 0 : (i % 2) + 1,
y: isSelected(project.id) ? 0 : Math.floor(i / 2) + (!isSelected(project.id) ? 1 : 0),
w: isSelected(project.id) ? 2 : 1,
h: project.images && project.images.length > 0 ? currentCellHeight : 1
};
}),
};
};
return (
<div style={{ padding: "48px 0"}}>
<div
style={{
marginBottom: 8,
fontFamily: "var(--mono)",
fontSize: 11,
color: "var(--muted)",
letterSpacing: "0.08em",
textTransform: "uppercase",
}}
>
{"# projects"}
</div>
<h2
style={{
fontFamily: "var(--sans)",
fontSize: 28,
fontWeight: 700,
color: "var(--fg)",
margin: "0 0 32px 0",
letterSpacing: "-0.01em",
}}
>
{"Things I've built"}
</h2>
<div ref={containerRef}>
{mounted && (<Responsive
layouts={layouts}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 2, md: 2, sm: 2, xs: 2, xxs: 2 }}
width={width}
compactor={horizontalCompactor}
rowHeight={rowHeight}
dragConfig={{enabled:false}}
>
{PROJECTS.map((project, i) => (
<div key={project.id} style={{pointerEvents: "none"}}>
<ProjectCard
project={project}
visible={visible.has(i)}
selected={selectedProject === project.id}
setSelected={setSelectedHandler}
setExpandedHeight={setExpandedHeight}
/>
</div>
))}
</Responsive>)}
</div>
</div>
);
}