From 39a056db253694ab272115f5ec0fb92e1b8b4c2f Mon Sep 17 00:00:00 2001 From: "Hunter W." Date: Tue, 2 Jun 2026 16:16:50 -0400 Subject: [PATCH] updated projects display; simplified for mobile --- app/globals.css | 5 +- components/ProjectCard.tsx | 17 +-- components/Projects.tsx | 210 +++++++++++++++++++---------------- components/SimpleGallery.tsx | 52 +++------ styles/gallery.css | 26 +++++ tsconfig.json | 2 +- 6 files changed, 170 insertions(+), 142 deletions(-) create mode 100644 styles/gallery.css diff --git a/app/globals.css b/app/globals.css index 49b1358..776418f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -143,10 +143,12 @@ body { background: rgba(110, 231, 183, 0.25); color: var(--fg); } + .footer { flex-direction: row; height: 28; } + @media (max-width: 640px) { h1 { font-size: 32px !important; @@ -155,4 +157,5 @@ body { flex-direction: column; height: 36; } -} \ No newline at end of file +} + diff --git a/components/ProjectCard.tsx b/components/ProjectCard.tsx index 39850e1..4f7c757 100644 --- a/components/ProjectCard.tsx +++ b/components/ProjectCard.tsx @@ -10,7 +10,7 @@ type Props = { visible: boolean; selected: boolean; setSelected: (selected: any) => void; - updateHeight: (activeId: number, contentRef: HTMLElement | null) => void; + updateHeight: (projectId: number, heightPx: number) => void; }; export default function ProjectCard({ project, visible, selected = false, setSelected, updateHeight}: Props) { @@ -45,9 +45,9 @@ export default function ProjectCard({ project, visible, selected = false, setSel onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onClick={() => setSelected(project.id)} + className="project-card" style={{ - // height: "100%", - minWidth: "10em", + width: "100%", textDecoration: "none", padding: 28, overflow: "hidden", @@ -86,12 +86,14 @@ export default function ProjectCard({ project, visible, selected = false, setSel ); } - useEffect(() => { + useEffect(() => { if (!contentRef.current) return; - const observer = new ResizeObserver(() => { - if (contentRef.current) { - updateHeight(project.id, contentRef.current); + const observer = new ResizeObserver((entries) => { + const height = entries[0]?.borderBoxSize?.[0]?.blockSize + ?? entries[0]?.contentRect.height; + if (height > 0) { + updateHeight(project.id, height); } }); @@ -106,6 +108,7 @@ export default function ProjectCard({ project, visible, selected = false, setSel ref={contentRef} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} + className="project-card" style={{ minWidth: "10em", overflow: "hidden", diff --git a/components/Projects.tsx b/components/Projects.tsx index f06756b..70e1fae 100644 --- a/components/Projects.tsx +++ b/components/Projects.tsx @@ -2,7 +2,7 @@ import { PROJECTS } from "@/data/content"; import { useStaggerReveal } from "@/hooks/useAnimations"; -import React, { useState, useCallback, useRef } from "react"; +import React, { useState, useCallback, useRef, useEffect } from "react"; import ProjectCard from "@/components/ProjectCard"; @@ -10,121 +10,94 @@ import { Layout, Responsive, useContainerWidth, - horizontalCompactor, DefaultBreakpoints, + verticalCompactor, } from "react-grid-layout"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; -export default function Projects() { - const rowHeight = 100; - const visible = useStaggerReveal(PROJECTS.length, 100); - const [selectedProject, setSelectedProject] = useState(1); - const { width, containerRef, mounted } = useContainerWidth(); +const ROW_HEIGHT = 100; +const SMALL_BREAKPOINT = 768; - const [layouts, setLayouts] = useState>( - () => buildLayouts(1, {}) - ); +function buildLayoutsFromHeights(activeId: number, heights: Record): Record { + const selected = PROJECTS.find((p) => p.id === activeId); + const rest = PROJECTS.filter((p) => p.id !== activeId); - const heightCache = useRef>({}); + const h = (id: number, fallback: number) => + heights[id.toString()] ?? fallback; - function buildLayouts( - activeId: number, - heightOverrides: Record -): Record { - const isSelected = (id: number) => id === activeId; + function twoCols(): Layout { + let items: Layout = []; + const selH = selected ? h(selected.id, 2) : 0; - function h(project: (typeof PROJECTS)[number]) { - const override = heightOverrides[project.id.toString()]; - if (isSelected(project.id) && override != null) return override; - if(!isSelected(project.id)) return 1; - return project.images?.length ? 2 : 1; - } - - const selectedProject = PROJECTS.find((p) => isSelected(p.id)); - const selectedH = selectedProject ? h(selectedProject) : 1; - const nonSelected = PROJECTS.filter((p) => !isSelected(p.id)); - - function twoCol(project: (typeof PROJECTS)[number]) { - if (isSelected(project.id)) { - return { i: project.id.toString(), x: 0, y: 0, w: 2, h: h(project) }; + + if (selected) { + items = items.concat({ + i: selected.id.toString(), + x: 0, + y: 0, + w: 2, + h: selH, + }); } - const ni = nonSelected.indexOf(project); - return { - i: project.id.toString(), - x: ni % 2, - y: Math.floor(ni / 2) + selectedH, - w: 1, - h: 3, - }; - } - function singleCol(project: (typeof PROJECTS)[number], colSpan: number) { - if (isSelected(project.id)) { - return { i: project.id.toString(), x: 0, y: 0, w: colSpan, h: h(project) }; + const colY = [selH, selH]; + for (const p of rest) { + const pH = h(p.id, 3); + const col = colY[0] <= colY[1] ? 0 : 1; + items = items.concat({ i: p.id.toString(), x: col, y: colY[col], w: 1, h: pH }); + colY[col] += pH; } - const ni = nonSelected.indexOf(project); - return { - i: project.id.toString(), - x: 0, - y: ni + selectedH, - w: colSpan, - h: project.images?.length ? 4 : 2, - }; + + return items; } return { - lg: PROJECTS.map((p) => twoCol(p)), - md: PROJECTS.map((p) => twoCol(p)), - sm: PROJECTS.map((p) => twoCol(p)), - xs: PROJECTS.map((p) => singleCol(p, 4)), - xxs: PROJECTS.map((p) => singleCol(p, 4)), + lg: twoCols(), + md: twoCols(), + sm: twoCols(), + + // small screens get the horizontal scroll row + xs: twoCols(), + xxs: twoCols(), }; } +/* ------------------------------------------------------------------ */ +/* Component */ +/* ------------------------------------------------------------------ */ + +export default function Projects() { + const visible = useStaggerReveal(PROJECTS.length, 100); + const { width, containerRef, mounted } = useContainerWidth(); + const isSmall = mounted && width > 0 && width < SMALL_BREAKPOINT; + + const [selectedProject, setSelectedProject] = useState(1); const selectedRef = useRef(selectedProject); selectedRef.current = selectedProject; - const pendingUpdate = useRef | null>(null); + const [layouts, setLayouts] = useState>(() => buildLayoutsFromHeights(1, {})); + const measuredHeights = useRef>({}); + +const updateHeight = useCallback((projectId: number, heightPx: number) => { + const key = projectId.toString(); + const rowH = Math.max(1, Math.ceil(heightPx / ROW_HEIGHT)); - const updateHeight = useCallback( - (sourceId: number, contentRef: HTMLElement | null) => { - if (sourceId !== selectedRef.current) return; + if (measuredHeights.current[key] === rowH) return; + measuredHeights.current[key] = rowH; - if (pendingUpdate.current) clearTimeout(pendingUpdate.current); - pendingUpdate.current = setTimeout(() => { - const newH = contentRef - ? Math.ceil(contentRef.getBoundingClientRect().height / rowHeight) - : 2; - - const key = sourceId.toString(); - if (heightCache.current[key] === newH) return; - heightCache.current[key] = newH; - setLayouts((prev) => { - const next = { ...prev }; - for (const bp in next) { - next[bp as DefaultBreakpoints] = next[bp as DefaultBreakpoints].map( - (layout) => - layout.i === key ? { ...layout, h: newH } : layout - ); - } - return next; - }); - }, 10); - }, - [rowHeight] -); + setLayouts(buildLayoutsFromHeights(selectedRef.current, measuredHeights.current)); +}, []); - function setSelectedHandler(projectId: number) { - const id = Number(projectId); - setSelectedProject(id); - heightCache.current = {}; - setLayouts(buildLayouts(id, {})); - } + function setSelectedHandler(projectId: number) { + setSelectedProject(projectId); + measuredHeights.current = {}; + setLayouts(buildLayoutsFromHeights(projectId, {})); +} - return ( -
+ const header = ( + <>
{"Things I've built"} + + ); + + /* ---- small screen: horizontal scroll row ---- */ + if (isSmall) { + return ( +
+ {header} +
+ {PROJECTS.map((project, i) => ( +
+ +
+ ))} +
+
+ ); + } + + /* ---- normal: grid layout ---- */ + return ( +
+ {header}
{mounted && ( {PROJECTS.map((project, i) => ( -
+
(e.currentTarget.style.background = "rgba(0,0,0,0.8)") } @@ -97,27 +85,13 @@ export default function SimpleGallery({ function shrinkButton() { return (