Skip to content

Instantly share code, notes, and snippets.

@iankit3
Created February 28, 2024 19:20
Show Gist options
  • Save iankit3/1b94a900d1cc9b09065653c86531e54f to your computer and use it in GitHub Desktop.
Save iankit3/1b94a900d1cc9b09065653c86531e54f to your computer and use it in GitHub Desktop.
Pinterest clone with Masonary
<div id="root"></div>
<template id="card-template">
<div class="card">
<div class="card-body"></div>
</div>
</template>
const root = document.getElementById("root");
//templates
const cardTemplate = document.getElementById("card-template");
function getImageUrl(width = 200, height = 300) {
const url = `https://source.unsplash.com/random/${width}x${height}`;
return new Promise((resolve, reject) => {
fetch(url, {
// manual tells fetch not to automatically follow redirects
redirect: "follow"
})
.then(async (response) => {
if (response.redirected) {
const url = new URL(response.url);
const urlSearchParams = new URLSearchParams(url.search);
// set the quality from 0-100
urlSearchParams.set("q", "2");
url.search = urlSearchParams.toString();
// const res = await fetch(url.toString());
// resolve(res.url)
resolve(url.toString());
} else {
resolve(resolve.url);
}
})
.catch((e) => {
console.error(e);
resolve(url);
});
});
}
function getRandomHeight() {
//const [min, max] = [100, 400];
const ranges = [100, 200, 150, 300, 250, 400, 350, 320, 290];
return ranges[0 + Math.floor(Math.random() * ranges.length)];
}
function getRandomBackground() {
const ranges = [100, 200, 150, 300, 250, 400, 350, 320, 290];
const getRandomBetween = (min, max) =>
Math.floor(min + ((Math.random() * max) % max));
const hsla = `hsla(${getRandomBetween(0, 360)}, ${getRandomBetween(
0,
100
)}%, ${getRandomBetween(0, 100)}%, ${Math.random().toFixed(2)})`;
return hsla;
}
function updateImageData(cardBody, width, height) {
getImageUrl(width, height).then((url) => {
const img = cardBody.querySelector("img.card-image");
img.dataset.src = url;
});
}
function createCard(i, topIndices) {
const node = cardTemplate.content.cloneNode(true);
const card = node.querySelector(".card");
const height = getRandomHeight();
const width = 200;
card.style.height = `${height}px`;
card.style.background = getRandomBackground();
const cardBody = node.querySelector(".card-body");
if (!topIndices.includes(i)) {
// Make this sync so that our intersection observer
// see the proper data-src
requestAnimationFrame(() => {
updateImageData(cardBody, width, height);
updateContent(cardBody, height, width, "", i, true);
});
} else {
requestAnimationFrame(async () => {
const src = await getImageUrl(width, height);
updateContent(cardBody, height, width, src, i, false);
});
}
return node;
}
function handleError(e) {
this.onerror = null;
console.log("Error loading image with url " + this.src + e);
event.target.classList.add("lazy");
//event.target.classList.add("hide");
requestAnimationFrame(() => loadLazyImages());
}
function updateContent(cardBody, height, width, src, caption, shouldLazyLoad) {
cardBody.innerHTML = `
<div class="card-image-container">
<figure>
<img class="card-image skeleton ${
shouldLazyLoad ? "lazy" : ""
}" src="${src}" data-src="${src}" height="${height}" width="${width}"
onerror="handleError(event)"
/>
<figcaption>${caption}</figcaption>
</figure>
</card>
`;
}
function loadLazyImages() {
var lazyImages = Array.from(document.querySelectorAll("img.lazy"));
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function (
entries,
observer
) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy");
//lazyImage.classList.remove("hide");
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazyImages.forEach(function (lazyImage) {
lazyImageObserver.observe(lazyImage);
});
}
}
function init() {
const totalElements = 40;
const totalCols = 4;
const topIndices = [];
for (let i = 0; i < totalCols; ++i) {
const x = i * Math.floor(totalElements / 4);
//topIndices.push(...[x - 1, x - 2]);
topIndices.push(...[x, x + 1, x + 2, x + 3]);
}
for (let i = 0; i < totalElements; ++i) {
const node = createCard(i, topIndices);
root.appendChild(node);
}
root.dataset.indices = topIndices;
requestAnimationFrame(() => loadLazyImages());
loadLazyImages();
}
(() => {
/*
## TODO:
- fix: overcall to loadLazyImages
- feat: impl infinite scroll
- feat: use virtual list or sentinal elem for adding elems [web.dev](https://web.dev/patterns/web-vitals-patterns/infinite-scroll/infinite-scroll#js)
### Using column-count css for generating masonary layout
- pros
- very easy, just a css prop column-count in the container
- cons
- layout flows in column
- due to this need to find first few rows to preload those images
```js
const totalElements = 50, totalCols = 4, topIndices = [];
for (let i = 0; i < totalCols; ++i) {
const x = i * Math.floor(totalElements / 4);
//topIndices.push(...[x - 1, x - 2]);
topIndices.push(...[x, x + 1, x + 2, x + 3]);
}
```
### Using intersection observer for lazy loading
### Using hacky splash url with redirection
*/
init();
const tmRef = setTimeout(() => {
// TODO: Fix this, if not fixable see who to avoid these race conditions whithout any performance penalty;
// run again to handle all the images where data-src got update a little late
loadLazyImages();
clearInterval(tmRef);
}, 2000);
})();
#root {
width: 800px;
border: 1px solid;
padding: 1ch;
background: #efefef;
column-count: 4;
column-gap: 5px;
margin: auto;
.card {
width: 200px;
height: 100px;
background: lightpink;
border: 2px solid lightgrey;
display: flex;
align-items: center;
justify-content: center;
align-self: flex-start;
margin: 0;
box-sizing: border-box;
overflow: hidden;
.card-image-container {
figure {
position: relative;
figcaption {
position: absolute;
margin-left: 50%;
transform: translateX(-50%);
bottom: 10px;
background: rgb(186 186 186 / 77%);
padding: 0.2em 0.5em;
border-radius: 4px;
}
}
.card-image {
aspect-ratio: 4 / 3;
object-fit: contain;
&.hide {
scale: 0;
}
}
}
}
}
.skeleton {
animation: skeleton-loading 1s ease-in-out infinite alternate;
}
@keyframes skeleton-loading {
0% {
background-color: lightgrey;
}
100% {
background-color: #efefef;
}
}
@iankit3
Copy link
Author

iankit3 commented Feb 28, 2024

https://codepen.io/iankit3/pen/zYXOmqx

TODO:

  - fix: overcall to loadLazyImages
  - feat: impl infinite scroll
  - feat: use virtual list or sentinal elem for adding elems [web.dev](https://web.dev/patterns/web-vitals-patterns/infinite-scroll/infinite-scroll#js) 

Using column-count css for generating masonary layout

  - pros
    - very easy, just a css prop column-count in the container
  - cons
    - layout flows in column
    - due to this need to find first few rows to preload those images
    const totalElements = 50, totalCols = 4, topIndices = [];
    for (let i = 0; i < totalCols; ++i) {
      const x = i * Math.floor(totalElements / 4);
      //topIndices.push(...[x - 1, x - 2]);
      topIndices.push(...[x, x + 1, x + 2, x + 3]);
    }

Using intersection observer for lazy loading

Using hacky unsplash url with redirection

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment