A Pen by Ankit Kumar on CodePen.
Created
February 28, 2024 19:20
-
-
Save iankit3/1b94a900d1cc9b09065653c86531e54f to your computer and use it in GitHub Desktop.
Pinterest clone with Masonary
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="root"></div> | |
<template id="card-template"> | |
<div class="card"> | |
<div class="card-body"></div> | |
</div> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://codepen.io/iankit3/pen/zYXOmqx
TODO:
Using column-count css for generating masonary layout
Using intersection observer for lazy loading
Using hacky unsplash url with redirection