grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

add masonry layout to the gallery page

+143 -50
+15 -13
main.tsx
··· 160 160 } 161 161 if (!gallery) return ctx.next(); 162 162 ctx.state.meta = getGalleryMeta(gallery); 163 - ctx.state.scripts = ["photo_dialog.js"]; 163 + ctx.state.scripts = ["photo_dialog.js", "masonry.js"]; 164 164 return ctx.render( 165 165 <GalleryPage favs={favs} gallery={gallery} currentUserDid={did} />, 166 166 ); ··· 374 374 ]; 375 375 return ctx.html( 376 376 <> 377 - <div hx-swap-oob="beforeend:#gallery-photo-grid"> 377 + <div hx-swap-oob="beforeend:#masonry-container"> 378 378 <PhotoButton 379 379 key={photo.cid} 380 380 photo={photoToView(photo.did, photo)} ··· 956 956 href="https://unpkg.com/@fortawesome/fontawesome-free@6.7.2/css/all.min.css" 957 957 preload 958 958 /> 959 - {scripts?.includes("photo_dialog.js") 960 - ? <script src="/static/photo_dialog.js" /> 961 - : null} 959 + {scripts?.map((file) => <script key={file} src={`/static/${file}`} />)} 962 960 </head> 963 961 <body class="h-full w-full dark:bg-zinc-950 dark:text-white"> 964 962 <Layout id="layout" class="dark:border-zinc-800"> ··· 1378 1376 > 1379 1377 <label htmlFor="file"> 1380 1378 <span class="sr-only">Upload avatar</span> 1381 - <div class="border rounded-full border-slate-900 w-16 h-16 mx-auto mb-2 relative my-2 cursor-pointer"> 1382 - <div class="absolute bottom-0 right-0 bg-slate-800 rounded-full w-5 h-5 flex items-center justify-center z-10"> 1379 + <div class="border rounded-full border-zinc-900 w-16 h-16 mx-auto mb-2 relative my-2 cursor-pointer"> 1380 + <div class="absolute bottom-0 right-0 bg-zinc-800 rounded-full w-5 h-5 flex items-center justify-center z-10"> 1383 1381 <i class="fa-solid fa-camera text-white text-xs"></i> 1384 1382 </div> 1385 1383 <div id="image-preview" class="w-full h-full"> ··· 1474 1472 : null} 1475 1473 </div> 1476 1474 <div 1477 - id="gallery-photo-grid" 1478 - class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4" 1475 + id="masonry-container" 1476 + class="h-0 overflow-hidden relative mx-auto w-full" 1477 + _="on load or htmx:afterSettle call computeMasonry()" 1479 1478 > 1480 1479 {gallery.items?.filter(isPhotoView)?.length 1481 1480 ? gallery?.items?.filter(isPhotoView)?.map((photo) => ( ··· 1507 1506 hx-trigger="click" 1508 1507 hx-target="#layout" 1509 1508 hx-swap="afterbegin" 1510 - class="cursor-pointer relative sm:aspect-square" 1509 + class="masonry-tile absolute cursor-pointer" 1510 + data-width={photo.aspectRatio?.width} 1511 + data-height={photo.aspectRatio?.height} 1511 1512 > 1512 1513 {isLoggedIn && isCreator 1513 1514 ? <AltTextButton galleryUri={gallery.uri} cid={photo.cid} /> ··· 1515 1516 <img 1516 1517 src={photo.fullsize} 1517 1518 alt={photo.alt} 1518 - class="sm:absolute sm:inset-0 w-full h-full sm:object-contain" 1519 + class="w-full h-full object-cover" 1519 1520 /> 1520 1521 {!isCreator && photo.alt 1521 1522 ? ( 1522 - <div class="absolute bg-zinc-950 dark:bg-zinc-900 bottom-2 right-2 sm:bottom-0 sm:right-0 text-xs text-white font-semibold py-[1px] px-[3px]"> 1523 + <div class="absolute bg-zinc-950 dark:bg-zinc-900 bottom-1 right-1 sm:bottom-1 sm:right-1 text-xs text-white font-semibold py-[1px] px-[3px]"> 1523 1524 ALT 1524 1525 </div> 1525 1526 ) ··· 1685 1686 }: Readonly<{ galleryUri: string; cid: string }>) { 1686 1687 return ( 1687 1688 <div 1688 - class="bg-zinc-950 dark:bg-zinc-900 py-[1px] px-[3px] absolute top-2 left-2 sm:top-0 sm:left-0 cursor-pointer flex items-center justify-center text-xs text-white font-semibold z-10" 1689 + class="bg-zinc-950 dark:bg-zinc-900 py-[1px] px-[3px] absolute top-1 left-1 sm:top-1 sm:left-1 cursor-pointer flex items-center justify-center text-xs text-white font-semibold z-10" 1689 1690 hx-get={`/dialogs/image-alt?galleryUri=${galleryUri}&imageCid=${cid}`} 1690 1691 hx-trigger="click" 1691 1692 hx-target="#layout" ··· 1785 1786 rows={4} 1786 1787 defaultValue={photo.alt} 1787 1788 placeholder="Alt text" 1789 + autoFocus 1788 1790 class="dark:bg-zinc-800 dark:text-white" 1789 1791 /> 1790 1792 </div>
+84
static/masonry.js
··· 1 + // deno-lint-ignore-file 2 + 3 + let masonryObserverInitialized = false; 4 + 5 + function computeMasonry() { 6 + const container = document.getElementById("masonry-container"); 7 + if (!container) return; 8 + 9 + const spacing = 12; 10 + const containerWidth = container.offsetWidth; 11 + 12 + if (containerWidth === 0) { 13 + requestAnimationFrame(computeMasonry); 14 + return; 15 + } 16 + 17 + const columns = containerWidth < 640 ? 1 : 3; 18 + 19 + const columnWidth = (containerWidth + spacing) / columns - spacing; 20 + const columnHeights = new Array(columns).fill(0); 21 + const tiles = container.querySelectorAll(".masonry-tile"); 22 + 23 + tiles.forEach((tile) => { 24 + const imgW = parseFloat(tile.dataset.width); 25 + const imgH = parseFloat(tile.dataset.height); 26 + if (!imgW || !imgH) return; 27 + 28 + const aspectRatio = imgH / imgW; 29 + const renderedHeight = aspectRatio * columnWidth; 30 + 31 + let shortestIndex = 0; 32 + for (let i = 1; i < columns; i++) { 33 + if (columnHeights[i] < columnHeights[shortestIndex]) { 34 + shortestIndex = i; 35 + } 36 + } 37 + 38 + const left = (columnWidth + spacing) * shortestIndex; 39 + const top = columnHeights[shortestIndex]; 40 + 41 + Object.assign(tile.style, { 42 + position: "absolute", 43 + width: `${columnWidth}px`, 44 + height: `${renderedHeight}px`, 45 + left: `${left}px`, 46 + top: `${top}px`, 47 + }); 48 + 49 + columnHeights[shortestIndex] = top + renderedHeight + spacing; 50 + }); 51 + 52 + container.style.height = `${Math.max(...columnHeights)}px`; 53 + } 54 + 55 + function observeMasonry() { 56 + if (masonryObserverInitialized) return; 57 + masonryObserverInitialized = true; 58 + 59 + const container = document.getElementById("masonry-container"); 60 + if (!container) return; 61 + 62 + // Observe parent resize 63 + if (typeof ResizeObserver !== "undefined") { 64 + const resizeObserver = new ResizeObserver(() => computeMasonry()); 65 + if (container.parentElement) { 66 + resizeObserver.observe(container.parentElement); 67 + } 68 + } 69 + 70 + // Observe inner content changes (tiles being added/removed) 71 + const mutationObserver = new MutationObserver(() => { 72 + computeMasonry(); 73 + }); 74 + 75 + mutationObserver.observe(container, { 76 + childList: true, 77 + subtree: true, 78 + }); 79 + } 80 + 81 + document.addEventListener("DOMContentLoaded", () => { 82 + computeMasonry(); 83 + observeMasonry(); 84 + });
+44 -37
static/styles.css
··· 208 208 .inset-0 { 209 209 inset: calc(var(--spacing) * 0); 210 210 } 211 - .top-0 { 212 - top: calc(var(--spacing) * 0); 211 + .top-1 { 212 + top: calc(var(--spacing) * 1); 213 213 } 214 214 .top-2 { 215 215 top: calc(var(--spacing) * 2); ··· 217 217 .right-0 { 218 218 right: calc(var(--spacing) * 0); 219 219 } 220 + .right-1 { 221 + right: calc(var(--spacing) * 1); 222 + } 220 223 .right-2 { 221 224 right: calc(var(--spacing) * 2); 222 225 } 223 226 .bottom-0 { 224 227 bottom: calc(var(--spacing) * 0); 228 + } 229 + .bottom-1 { 230 + bottom: calc(var(--spacing) * 1); 225 231 } 226 232 .bottom-2 { 227 233 bottom: calc(var(--spacing) * 2); ··· 232 238 .left-0 { 233 239 left: calc(var(--spacing) * 0); 234 240 } 235 - .left-2 { 236 - left: calc(var(--spacing) * 2); 241 + .left-1 { 242 + left: calc(var(--spacing) * 1); 237 243 } 238 244 .z-10 { 239 245 z-index: 10; ··· 244 250 .z-30 { 245 251 z-index: 30; 246 252 } 253 + .container { 254 + width: 100%; 255 + @media (width >= 40rem) { 256 + max-width: 40rem; 257 + } 258 + @media (width >= 48rem) { 259 + max-width: 48rem; 260 + } 261 + @media (width >= 64rem) { 262 + max-width: 64rem; 263 + } 264 + @media (width >= 80rem) { 265 + max-width: 80rem; 266 + } 267 + @media (width >= 96rem) { 268 + max-width: 96rem; 269 + } 270 + } 247 271 .mx-auto { 248 272 margin-inline: auto; 249 273 } ··· 293 317 .size-16 { 294 318 width: calc(var(--spacing) * 16); 295 319 height: calc(var(--spacing) * 16); 320 + } 321 + .h-0 { 322 + height: calc(var(--spacing) * 0); 296 323 } 297 324 .h-1\/2 { 298 325 height: calc(1/2 * 100%); ··· 351 378 .cursor-pointer { 352 379 cursor: pointer; 353 380 } 381 + .resize { 382 + resize: both; 383 + } 354 384 .grid-cols-1 { 355 385 grid-template-columns: repeat(1, minmax(0, 1fr)); 356 386 } ··· 406 436 border-style: var(--tw-border-style); 407 437 border-width: 1px; 408 438 } 409 - .border-slate-900 { 410 - border-color: var(--color-slate-900); 439 + .border-zinc-900 { 440 + border-color: var(--color-zinc-900); 411 441 } 412 442 .bg-black { 413 443 background-color: var(--color-black); ··· 417 447 @supports (color: color-mix(in lab, red, red)) { 418 448 background-color: color-mix(in oklab, var(--color-black) 80%, transparent); 419 449 } 420 - } 421 - .bg-slate-800 { 422 - background-color: var(--color-slate-800); 423 450 } 424 451 .bg-zinc-100 { 425 452 background-color: var(--color-zinc-100); ··· 549 576 opacity: 50%; 550 577 } 551 578 } 552 - .sm\:absolute { 553 - @media (width >= 40rem) { 554 - position: absolute; 555 - } 556 - } 557 - .sm\:inset-0 { 558 - @media (width >= 40rem) { 559 - inset: calc(var(--spacing) * 0); 560 - } 561 - } 562 - .sm\:top-0 { 579 + .sm\:top-1 { 563 580 @media (width >= 40rem) { 564 - top: calc(var(--spacing) * 0); 565 - } 566 - } 567 - .sm\:right-0 { 568 - @media (width >= 40rem) { 569 - right: calc(var(--spacing) * 0); 581 + top: calc(var(--spacing) * 1); 570 582 } 571 583 } 572 - .sm\:bottom-0 { 584 + .sm\:right-1 { 573 585 @media (width >= 40rem) { 574 - bottom: calc(var(--spacing) * 0); 586 + right: calc(var(--spacing) * 1); 575 587 } 576 588 } 577 - .sm\:left-0 { 589 + .sm\:bottom-1 { 578 590 @media (width >= 40rem) { 579 - left: calc(var(--spacing) * 0); 591 + bottom: calc(var(--spacing) * 1); 580 592 } 581 593 } 582 - .sm\:aspect-square { 594 + .sm\:left-1 { 583 595 @media (width >= 40rem) { 584 - aspect-ratio: 1 / 1; 596 + left: calc(var(--spacing) * 1); 585 597 } 586 598 } 587 599 .sm\:h-screen { ··· 617 629 .sm\:justify-between { 618 630 @media (width >= 40rem) { 619 631 justify-content: space-between; 620 - } 621 - } 622 - .sm\:object-contain { 623 - @media (width >= 40rem) { 624 - object-fit: contain; 625 632 } 626 633 } 627 634 .sm\:px-0 {