Created
July 24, 2022 22:42
-
-
Save omargfh/42d9ecd243c19b064832b13d13b57a8f to your computer and use it in GitHub Desktop.
Next.js Bootstrap Gallery with Image Viewer
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
import { useEffect, useState } from "react"; | |
import Image from 'next/image'; | |
function Gallery({images, viewerHandler}) { | |
const [active, setActive] = useState(""); | |
const [scaleUpAnimation, setScaleUpAnimation] = useState(""); | |
const root = images.root; | |
images = images.paths; | |
const handleRibbonClick = (dir) => { | |
setScaleUpAnimation(""); | |
if (dir == active) { | |
setActive(""); | |
} | |
else { | |
setActive(dir); | |
} | |
setTimeout(() => { | |
setScaleUpAnimation("scale-up"); | |
}, 10); | |
} | |
const constructRibbon = () => { | |
let constructed = []; | |
for (const [dir, paths] of Object.entries(images)) { | |
constructed.push( | |
<div key={dir} className={"gallery-btn" + (active == dir ? " active" : "")} onClick={() => handleRibbonClick(dir)}> | |
{dir} | |
</div> | |
); | |
} | |
return ( | |
<div className="gallery-ribbon"> | |
<div key="all" className={"gallery-btn" + (active == "" ? " active" : "")} onClick={() => handleRibbonClick("")}> | |
All | |
</div> | |
{constructed} | |
</div> | |
); | |
} | |
const constructImages = () => { | |
let constructed = []; | |
// Handles Bootstrap classes | |
let bootstrapSum = 0; | |
let seed = 510; | |
const nextBootstrapCol = () => { | |
bootstrapSum == 12 ? bootstrapSum = 0 : bootstrapSum = bootstrapSum; | |
let random = Math.sin(seed++) * 10000; | |
random = random - Math.floor(random); | |
if (random > 0.8 && bootstrapSum <= 4) { | |
bootstrapSum += 8; | |
return 'col-md-8'; | |
} | |
else { | |
bootstrapSum += 4; | |
return 'col-md-4'; | |
} | |
} | |
// Construscts JSX | |
for (const [dir, paths] of Object.entries(images)) { | |
for (const path of paths) { | |
const a = root + '/' + dir + '/' + path; | |
const next = nextBootstrapCol(); | |
constructed.push( | |
<div | |
key={a} | |
className={"gallery-image " + dir + (dir == active || active == "" ? " active " + scaleUpAnimation : " hidden" ) + ` ${next}`} | |
alt={"A picture of the " + dir} | |
onClick={()=>viewerHandler(a)}> | |
<Image layout="fill" src={a} /> | |
</div> | |
); | |
} | |
} | |
// Returns constructed Div | |
return ( | |
<div className="images row"> | |
{constructed} | |
</div> | |
); | |
} | |
return ( | |
<div className="section container gallery"> | |
{constructRibbon()} | |
{constructImages()} | |
</div> | |
) | |
} | |
export default Gallery |
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
.gallery { | |
.images { | |
position: relative; | |
.gallery-image { | |
overflow: hidden; | |
position: relative; | |
box-sizing: border-box; | |
border-top: 20px solid white; | |
border-bottom: 5px solid rgb(255, 255, 255); | |
border-left: 10px solid rgb(255, 255, 255); | |
border-right: 10px solid rgb(255, 255, 255); | |
transition: all 1s ease-in-out; | |
transform: translateY(0) scale(1); | |
&.col-md-4::after { | |
content: ""; | |
display: block; | |
padding-bottom: 100%; | |
} | |
&.col-md-8::after { | |
content: ""; | |
display: block; | |
padding-bottom: 50%; | |
} | |
img { | |
position: absolute!important; | |
top: 0%; | |
left: 0%; | |
right: 0%; | |
bottom: 0%; | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
object-position: center; | |
transition: all 0.5s ease-in-out; | |
} | |
&:hover { | |
cursor: pointer; | |
img { | |
transform: scale(1.2); | |
} | |
} | |
&::before { | |
content: ""; | |
position: absolute!important; | |
top: 0%; | |
left: 0%; | |
right: 0%; | |
bottom: 0%; | |
width: 100%; | |
height: 100%; | |
z-index: 12; | |
transition: all 0.4s ease-in-out; | |
} | |
&.scale-up { | |
top: 0; | |
animation-name: scale-up; | |
animation-duration: 1s; | |
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); | |
animation-fill-mode: forwards; | |
transform: translateY(0) scale(1); | |
} | |
} | |
@media (max-width: 776px) { | |
.gallery-image.col-md-8::after { | |
padding-bottom: 100%!important; | |
} | |
} | |
@keyframes scale-up { | |
0% { | |
transform: translateY(300px) scale(0); | |
} | |
100% { | |
display: none; | |
transform: translateY(0) scale(1); | |
} | |
} | |
} | |
.gallery-btn { | |
width: unset; | |
font: arial, sans-serif; | |
font-weight: bold; | |
font-size: 20px; | |
color: var(--grey); | |
display: inline; | |
margin-right: 1em; | |
&:hover, &.active { | |
text-decoration: underline; | |
cursor: pointer; | |
color: var(--light__maroon); | |
} | |
} | |
} | |
.image-viewer-container { | |
z-index: 5000; | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100vw; | |
height: 100vh; | |
background-color: rgba(0,0,0,.0); | |
animation: blur; | |
animation-duration: 0.6s; | |
animation-timing-function: ease-out; | |
animation-fill-mode: forwards; | |
overflow: hidden; | |
.image-viewer-app { | |
width: 100vw; | |
height: 100vh; | |
position: relative; | |
color: white; | |
font-size: 24px; | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
.left-arrow, .right-arrow { | |
position: absolute; | |
top: 50%; | |
transform: translateY(-50%); | |
padding: 1em; | |
background-color: rgba(0,0,0,.6); | |
&:hover { | |
cursor: pointer; | |
background-color: black; | |
} | |
} | |
.left-arrow { | |
right: 0; | |
} | |
.right-arrow { | |
left: 0; | |
} | |
.close { | |
position: absolute; | |
top: 0; | |
right: 0; | |
margin: 0.5em 0 0 0; | |
padding: 0.5em 0.7em; | |
background-color: var(--dark__maroon); | |
color: white; | |
&:hover { | |
cursor: pointer; | |
background-color: var(--light__maroon); | |
} | |
} | |
} | |
.activeImage { | |
height: 70%; | |
max-width: 80%; | |
overflow: hidden; | |
margin: 2em 0 2em 0; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
img { | |
height: 100%; | |
margin: 0 auto; | |
box-shadow: 0 0 8px rgb(0 0 0 / 60%); | |
border-radius: 5px; | |
} | |
} | |
.images { | |
max-height: 14%; | |
margin-bottom: 1em; | |
display: flex; | |
flex-direction: row; | |
width: 100%; | |
transition: transform 0.5s ease-in-out; | |
.image-thumbnail { | |
transform: scale(0.9); | |
transition: transform 0.5s ease-in-out; | |
&.active { | |
transform: scale(1.3); | |
z-index: 20; | |
} | |
} | |
img { | |
margin: 0 0px; | |
height: 100px; | |
width: 100px; | |
object-fit: cover; | |
object-position: center; | |
box-shadow: 0 0 8px rgb(0 0 0 / 60%); | |
} | |
&:hover { | |
cursor: pointer; | |
} | |
} | |
@media (max-width: 767px) { | |
.images { | |
display: none;; | |
} | |
} | |
@keyframes blur { | |
0% { | |
backdrop-filter: blur(0px); | |
background-color: rgba(0,0,0,.0); | |
} | |
100% { | |
background-color: rgba(0,0,0,.8); | |
} | |
} | |
} |
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
import { useEffect, useState } from "react"; | |
import Image from 'next/image'; | |
import { gsap } from "gsap/dist/gsap"; | |
import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; | |
export default function ImageViewer({images, invokedImage, imageViewerActive, close}) { | |
const root = images.root; | |
images = images.paths; | |
const [activeImage, setActiveImage] = useState(invokedImage); | |
useEffect(() => { | |
setActiveImage(invokedImage); | |
}, [invokedImage]); | |
const getAllPaths = () => { | |
const ret = []; | |
Object.entries(images).forEach(([dir, paths]) => { | |
paths.forEach((path) => { | |
ret.push(root + '/' + dir + '/' + path); | |
}) | |
}); | |
return ret; | |
} | |
const handleClick = (paths, direction) => { | |
const increment = direction == 'left' ? 1 : -1; | |
const index = paths.indexOf(activeImage) + increment; | |
if (index < 0) { | |
index = paths.length - 1; | |
} | |
else if (index >= paths.length) { | |
index = 0; | |
} | |
setActiveImage(paths[index]); | |
} | |
// Cleanup | |
useEffect(() => { | |
return () => { | |
setActiveImage(null); | |
}; | |
}, []); | |
const getOffset = (pathsForViewer) => { | |
const index = pathsForViewer.indexOf(activeImage); | |
const screenWidth = window.innerWidth; | |
const width = 100; | |
return -1 * index * width + (0.5 * (screenWidth - width)); | |
} | |
const constructImageViewer = () => { | |
const pathsForViewer = getAllPaths(); | |
return ( | |
<div className="image-viewer-container"> | |
<div className="image-viewer-app"> | |
<div className="activeImage"> | |
<img src={pathsForViewer.includes(activeImage) ? activeImage : 'images/error.jpg'} /> | |
</div> | |
<div className="close" onClick={() => close()}><i className="fa-solid fa-x"></i></div> | |
<div className="right-arrow" onClick={()=>handleClick(pathsForViewer, 'right')}><i className="fa-solid fa-chevron-left"></i></div> | |
<div className="left-arrow" onClick={()=>handleClick(pathsForViewer, 'left')}><i className="fa-solid fa-chevron-right"></i></div> | |
<div className="images" style={{ | |
transform: `translate3d(${(getOffset(pathsForViewer))}px, 0, 0)` | |
}}> | |
{ | |
pathsForViewer.map((src) => | |
<div key={src} className={"image-thumbnail" + (activeImage == src ? " active" : "")}> | |
<Image width="100" height="100" src={src} className={activeImage == src ? "active" : ""} onClick={()=>setActiveImage(src)} loading="lazy" alt={`A gallery thumbnail.`}/> | |
</div> | |
) | |
} | |
</div> | |
</div> | |
</div> | |
) | |
} | |
return ( | |
<> | |
{imageViewerActive ? constructImageViewer() : <></>} | |
</> | |
) | |
} |
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
import {useRef, useState} from 'react'; | |
import fs from 'fs'; | |
import Gallery from '../components/gallery/gallery'; | |
import ImageViewer from '../components/gallery/imageViewer'; | |
export default function Home({images}) { | |
// Image viewer and Gallery | |
const [invokedImage, setInvokedImage] = useState(null); | |
const [imageViewerActive, setImageViewerActive] = useState(false); | |
const handleImageClick = (fullPathName) => { | |
setInvokedImage(fullPathName); | |
setImageViewerActive(true); | |
} | |
const closeViewer = () => { | |
setImageViewerActive(false); | |
} | |
return ( | |
<div className="app"> | |
<ImageViewer images={images} invokedImage={invokedImage} imageViewerActive={imageViewerActive} close={closeViewer} /> | |
<main> | |
<Gallery images={images} viewerHandler={handleImageClick} /> | |
</main> | |
</div> | |
) | |
} | |
export async function getStaticProps(context) { | |
const root = './public'; | |
const path = '/images/landing'; | |
const fileList = await fs.readdirSync(root + path); | |
let images = { | |
"root": path, | |
"paths": {} | |
}; | |
for (const file of fileList) { | |
const stat = fs.statSync(root+path+'/'+file); | |
console.log(path+'/'+file); | |
// If it is a directory | |
if (!stat.isFile()) { | |
const subDirList = await fs.readdirSync(root + path + '/' + file); | |
images.paths[file] = subDirList; | |
} | |
} | |
return { | |
props: { | |
images | |
}, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment