diff --git a/src/components/actor-app.tsx b/src/components/actor-app.tsx index f94bf2d..e10ef81 100644 --- a/src/components/actor-app.tsx +++ b/src/components/actor-app.tsx @@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from "react"; import { BlueskyProfile } from "~/components/bluesky-profile"; import { HauntSigil } from "~/components/haunt-sigil"; import { IdentityCard } from "~/components/identity-card"; -import { MasonryGrid } from "~/components/masonry-grid"; import { ProfileSections } from "~/components/profile-sections"; import { ProfileSigilArtifact } from "~/components/profile-sigil-artifact"; import { resolveMiniDoc } from "~/lib/atproto/mini-doc"; @@ -124,7 +123,11 @@ export function ActorApp() { > - + {profile ? ( ) : ( @@ -132,13 +135,7 @@ export function ActorApp() { )} - - - + ); } diff --git a/src/components/masonry-grid.tsx b/src/components/masonry-grid.tsx index 6f78df4..0119986 100644 --- a/src/components/masonry-grid.tsx +++ b/src/components/masonry-grid.tsx @@ -1,131 +1,63 @@ import type { ReactNode } from "react"; -import { useLayoutEffect, useRef } from "react"; +import { Children, useLayoutEffect, useState } from "react"; -import { hashString } from "~/lib/did-random"; - -const MASONRY_MEDIA_QUERY = "(min-width: 40rem)"; +const THREE_COLUMN_BREAKPOINT = 64 * 16; +const TWO_COLUMN_BREAKPOINT = 40 * 16; type MasonryGridProps = { children: ReactNode; - seed: string; }; -export function MasonryGrid({ children, seed }: MasonryGridProps) { - const gridRef = useRef(null); - - useLayoutEffect(() => { - const masonryGrid = gridRef.current; - if (!masonryGrid) { - return; - } - - const gridElement: HTMLDivElement = masonryGrid; - - const mediaQuery = window.matchMedia(MASONRY_MEDIA_QUERY); - let animationFrame = 0; - - function scheduleLayout() { - cancelAnimationFrame(animationFrame); - animationFrame = requestAnimationFrame(updateLayout); - } - - function updateLayout() { - const cards = Array.from(gridElement.children) as HTMLElement[]; - - if (!mediaQuery.matches) { - gridElement.classList.remove("profile-grid-masonry"); - cards.forEach(clearCardLayout); - return; - } - - gridElement.classList.add("profile-grid-masonry"); - - const styles = window.getComputedStyle(gridElement); - const rowHeight = Number.parseFloat(styles.gridAutoRows); - const rowGap = Number.parseFloat(styles.rowGap); - const columnCount = getColumnCount(styles.gridTemplateColumns); +export function MasonryGrid({ children }: MasonryGridProps) { + const columnCount = useColumnCount(); + const columns = Array.from({ length: columnCount }, () => [] as ReactNode[]); - if (!rowHeight || !columnCount) { - return; - } - - const columnHeights = Array(columnCount).fill(0); - - cards.forEach((card, index) => { - const columnIndex = getShortestColumn(columnHeights); - - card.style.gridColumnStart = String(columnIndex + 1); - const rowSpan = getRowSpan(card, rowHeight, rowGap); - - card.style.gridRowStart = String(columnHeights[columnIndex]! + 1); - card.style.gridRowEnd = `span ${rowSpan}`; - setArtifactOffset(card, seed, index); - columnHeights[columnIndex]! += rowSpan; - }); - } - - const resizeObserver = new ResizeObserver(scheduleLayout); - const mutationObserver = new MutationObserver(scheduleLayout); - - resizeObserver.observe(gridElement); - mutationObserver.observe(gridElement, { childList: true }); - mediaQuery.addEventListener("change", scheduleLayout); - scheduleLayout(); - - return () => { - cancelAnimationFrame(animationFrame); - resizeObserver.disconnect(); - mutationObserver.disconnect(); - mediaQuery.removeEventListener("change", scheduleLayout); - }; - }, [seed]); + Children.toArray(children).forEach((artifact, index) => { + const columnIndex = index % columnCount; + columns[columnIndex]!.push(artifact); + }); return ( -
- {children} +
+ {columns.map((artifacts, index) => ( +
+ {artifacts} +
+ ))}
); } -function clearCardLayout(card: HTMLElement) { - card.style.removeProperty("grid-column-start"); - card.style.removeProperty("grid-row-start"); - card.style.removeProperty("grid-row-end"); - card.style.removeProperty("--artifact-offset-x"); - card.style.removeProperty("--artifact-offset-y"); -} +function useColumnCount(): number { + const [columnCount, setColumnCount] = useState(getColumnCount); -function setArtifactOffset(card: HTMLElement, seed: string, index: number) { - const hash = hashString(`${seed}:artifact:${index}`); - const horizontalOffset = (hash % 17) - 8; - const verticalOffset = ((hash >>> 5) % 13) - 6; + useLayoutEffect(() => { + function updateColumnCount() { + setColumnCount(getColumnCount()); + } - card.style.setProperty("--artifact-offset-x", `${horizontalOffset}px`); - card.style.setProperty("--artifact-offset-y", `${verticalOffset}px`); -} + window.addEventListener("resize", updateColumnCount); + updateColumnCount(); -function getColumnCount(gridTemplateColumns: string): number { - if (gridTemplateColumns === "none") { - return 0; - } + return () => window.removeEventListener("resize", updateColumnCount); + }, []); - return gridTemplateColumns.split(" ").length; + return columnCount; } -function getShortestColumn(columnHeights: number[]): number { - return columnHeights.reduce( - (shortestIndex, height, index) => - height < columnHeights[shortestIndex]! ? index : shortestIndex, - 0, - ); -} +function getColumnCount(): number { + if (window.innerWidth >= THREE_COLUMN_BREAKPOINT) { + return 3; + } -function getRowSpan( - card: HTMLElement, - rowHeight: number, - rowGap: number, -): number { - return Math.ceil( - (card.getBoundingClientRect().height + rowGap) / (rowHeight + rowGap), - ); + if (window.innerWidth >= TWO_COLUMN_BREAKPOINT) { + return 2; + } + + return 1; } diff --git a/src/components/plyr-tracks.tsx b/src/components/plyr-tracks.tsx index b78c611..7eb019f 100644 --- a/src/components/plyr-tracks.tsx +++ b/src/components/plyr-tracks.tsx @@ -44,11 +44,13 @@ export function PlyrTrackArtifact({ track }: { track: PlyrTrack }) { } function formatDuration(duration: number): string | null { - if (!Number.isFinite(duration)) { + const parsedDuration = Number(duration); + + if (!Number.isFinite(parsedDuration)) { return null; } - const totalSeconds = Math.max(0, Math.round(duration)); + const totalSeconds = Math.max(0, Math.round(parsedDuration)); const seconds = totalSeconds % 60; const minutes = Math.floor(totalSeconds / 60) % 60; const hours = Math.floor(totalSeconds / 3600); diff --git a/src/components/profile-sections.tsx b/src/components/profile-sections.tsx index 0c03956..bddf372 100644 --- a/src/components/profile-sections.tsx +++ b/src/components/profile-sections.tsx @@ -1,6 +1,8 @@ +import type { ReactNode } from "react"; import { useEffect, useState } from "react"; import { GrainPhotoArtifact } from "~/components/grain-photos"; +import { MasonryGrid } from "~/components/masonry-grid"; import { PlyrTrackArtifact } from "~/components/plyr-tracks"; import { ProfileArtifact } from "~/components/profile-artifact"; import { RockskyScrobble } from "~/components/rocksky-scrobble-list"; @@ -14,6 +16,7 @@ import { getMostStarredTangledRepos } from "~/lib/providers/tangled"; import { getRecentStandardDocuments } from "~/lib/providers/standard-site"; type ProfileSectionsProps = { + children: ReactNode; did: string; handle: string | null; pds: string; @@ -27,7 +30,12 @@ type ProfileData = { tangledRepos: Awaited>; }; -export function ProfileSections({ did, handle, pds }: ProfileSectionsProps) { +export function ProfileSections({ + children, + did, + handle, + pds, +}: ProfileSectionsProps) { const [data, setData] = useState(null); useEffect(() => { @@ -57,10 +65,13 @@ export function ProfileSections({ did, handle, pds }: ProfileSectionsProps) { if (!data) { return ( - -

Gathering signals

-

Searching the Atmosphere…

-
+ + {children} + +

Gathering signals

+

Searching the Atmosphere…

+
+
); } @@ -99,5 +110,10 @@ export function ProfileSections({ did, handle, pds }: ProfileSectionsProps) { )), ]; - return <>{getDidSectionOrder(did, artifacts)}; + return ( + + {children} + {getDidSectionOrder(did, artifacts)} + + ); } diff --git a/src/styles/globals.css b/src/styles/globals.css index 9235597..56dfc53 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -290,20 +290,13 @@ body { .profile-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); - align-items: start; gap: 1.5rem; } -.profile-grid-masonry { - grid-auto-rows: 0.5rem; -} - -.profile-grid-masonry > * { - transform: translate( - var(--artifact-offset-x, 0), - var(--artifact-offset-y, 0) - ); +.profile-grid-column { + display: flex; + flex-direction: column; + gap: 1.5rem; } .profile-card {