:root {
--padding-offset: 2rem;
--transition-duration-300: 300ms;
--transition-duration-100: 100ms;
--transition-delay: 80ms;
--body-background: rgb(225, 225, 225);
--white: #fff;
--black: #000;
--gray-light: #6d6d6d;
--gray-dark: #494949;
--blue-tint-light: hsl(216, 100%, 50%);
--blue-tint-dark: hsl(216, 100%, 35%);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100%;
min-height: 100vh;
background: var(--body-background);
font-family: Ubuntu, "Montserrat", sans-serif;
overflow-x: hidden;
}
input,
button {
border: none;
outline: none;
}
button {
cursor: pointer;
&:disabled {
cursor: not-allowed;
}
&:focus {
outline: none;
}
}
header {
position: relative;
width: 100%;
height: 80px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 2rem;
margin-bottom: 2rem;
.btn.home {
display: flex;
justify-content: center;
align-items: center;
background: none;
.icon {
--size: 30px;
width: var(--size);
height: var(--size);
svg {
fill: var(--blue-tint-light);
width: 100%;
height: 100%;
animation: hue-rotation 5000ms linear alternate-reverse infinite;
animation-play-state: paused;
}
}
span {
color: var(--gray-dark);
font-size: 1.2rem;
font-weight: bold;
text-transform: uppercase;
margin-left: 0.5rem;
}
&:hover {
.icon {
svg {
animation-play-state: running;
}
}
}
}
form {
display: flex;
justify-content: center;
align-items: center;
input[type="text"] {
width: 100%;
height: 100%;
padding: 10px 15px;
padding-right: 35%;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
&::placeholder {
color: rgba(0, 0, 0, 0.45);
}
}
button.search {
background: var(--blue-tint-light);
color: var(--white);
transition: background var(--transition-duration-100) ease,
filter var(--transition-duration-100) ease;
&:not(:disabled) {
filter: grayscale(0);
&:hover,
&:focus {
background: var(--blue-tint-dark);
}
}
&:disabled {
filter: grayscale(0.98);
}
}
.form-group {
--border-radius: 0.5em;
position: relative;
width: 300px;
border-radius: var(--border-radius);
overflow: hidden;
.icon {
--size: 12px;
width: var(--size);
height: var(--size);
transition: margin var(--transition-duration-100) ease;
svg {
fill: var(--white);
width: 100%;
height: 100%;
}
}
&.g-1 {
input[type="text"] {
&:focus {
~ button {
&:not(:disabled) {
transform: translateX(-55%);
.icon {
margin-right: 4px;
}
}
}
}
}
.btn.search {
position: absolute;
top: 0;
right: -20%;
transform: translateX(-50%);
width: 32%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
transition: transform var(--transition-duration-100) ease;
padding-right: 15px;
padding-left: 0;
span {
margin-left: 5px;
}
}
}
&.g-2 {
width: auto;
button.search {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
background: none;
.icon {
--size: 20px;
svg {
fill: var(--gray-light);
}
}
}
}
}
.floating--container {
position: absolute;
left: 0%;
top: 0%;
width: 100%;
height: 130%;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
background: var(--gray-dark);
transition: transform var(--transition-duration-300) ease;
.input--container {
width: 80%;
height: 40%;
display: grid;
grid-template-columns: 0.75fr 0.25fr;
border-radius: 0.4rem;
overflow: hidden;
margin-top: 0.8rem;
input[type="text"] {
padding-right: 1rem;
&:focus {
~ button {
&:not(:disabled):hover {
background: var(--blue-tint-dark);
}
}
}
}
button.search {
width: 100%;
height: 100%;
}
}
button.close {
--size: 30px;
width: var(--size);
height: var(--size);
display: flex;
justify-content: center;
align-items: center;
background: none;
.icon {
width: calc(var(--size) / 1.6);
height: calc(var(--size) / 1.6);
svg {
width: 100%;
height: 100%;
fill: #fb3958;
}
}
}
&.hide {
transform: translateY(-100%);
}
}
}
}
.masonry {
width: 100%;
height: auto;
padding: 0 1rem 1rem 1rem;
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-auto-rows: 0;
.masonry-brick {
.image-card {
--card-options-height: 60px;
position: relative;
display: flex;
background: var(--white);
box-shadow: 0 6.7px 5.3px rgba(0, 0, 0, 0.016), 0 22.3px 17.9px rgba(0, 0, 0, 0.024),
0 100px 80px rgba(0, 0, 0, 0.04);
overflow: hidden;
&::after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, transparent 100%);
opacity: 0;
transition: opacity var(--transition-duration-300) ease;
transition-delay: var(--transition-delay);
z-index: 1;
}
&:hover {
&::after {
opacity: 1;
}
img.image {
--image-transform-offset: -30px;
}
.image-card--options {
--tranform-y: 0;
}
}
img.image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--transition-duration-300) ease;
transform: translateY(var(--image-transform-offset, 0%));
transition-delay: var(--transition-delay);
}
&--clickable-area {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: calc(100% - var(--card-options-height));
z-index: 20;
cursor: pointer;
}
&--options {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: var(--card-options-height);
display: grid;
grid-template-columns: 1fr 0.35fr;
align-items: center;
background: var(--white);
transition: transform var(--transition-duration-300) ease;
transform: translateY(var(--tranform-y, 100%));
transition-delay: var(--transition-delay);
z-index: 5;
cursor: default;
.user-info {
height: 100%;
display: flex;
align-items: center;
padding: 0 0.5em;
a {
width: 35px;
height: 35px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
h3 {
margin-left: 6px;
font-size: 0.8em;
}
}
.links {
width: 100%;
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
padding-right: 0.8em;
.link {
.icon {
--size: 20px;
width: var(--size);
height: var(--size);
margin-left: 12px;
svg {
fill: var(--fill, rgba(0, 0, 0, 0.6));
width: 100%;
height: 100%;
transition: fill var(--transition-duration-100) ease;
}
}
&:hover {
.icon {
&.instagram {
--fill: #fb3958;
}
&.twitter {
--fill: #00acee;
}
&.download {
--fill: var(--black);
}
}
}
}
}
}
.image-card-fg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 10;
transform-origin: left center;
transform: scaleX(1);
transition: transform var(--transition-duration-300) ease;
pointer-events: none;
&.hide {
transform-origin: right center;
transform: scaleX(0);
}
}
}
}
}
.modal {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 35;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(2px);
display: flex;
justify-content: center;
align-items: center;
.image-container {
--icon-size: 20px;
position: relative;
width: auto;
margin-top: -2rem;
img {
height: 100%;
object-fit: cover;
}
button.close {
position: absolute;
top: 0;
right: calc(-2 * var(--icon-size));
transform: translate(-50%, -50%);
width: calc(var(--icon-size) * 2);
height: calc(var(--icon-size) * 2);
background: rgb(255, 79, 79);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.icon {
width: calc(var(--icon-size));
height: calc(var(--icon-size));
svg {
fill: var(--white);
width: 100%;
height: 100%;
}
}
.options--container {
position: absolute;
bottom: 0;
left: 0;
transform: translateY(100%);
width: 100%;
height: 50px;
display: flex;
justify-content: flex-end;
align-items: center;
background: var(--white);
border-bottom-left-radius: 0.4rem;
border-bottom-right-radius: 0.4rem;
.icon {
svg {
fill: var(--gray-light);
}
}
.group {
display: flex;
height: 32px;
margin: 0 10px;
a,
button {
--border-color: rgba(0, 0, 0, 0.25);
color: var(--gray-light);
font-size: 0.85rem;
text-transform: capitalize;
font-weight: 800;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border: 1px solid var(--border-color);
background: var(--white);
border-radius: 4px;
transition: border var(--transition-duration-100) ease,
color var(--transition-duration-100) ease;
&:hover {
--border-color: rgba(0, 0, 0, 1);
border: 1px solid var(--border-color);
color: var(--black);
.icon {
svg {
fill: var(--black);
}
}
}
}
&.g-1 {
a {
text-decoration: none;
padding: 0 15px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.dropdown-btn--container {
--size: 32px;
position: relative;
width: var(--size);
height: var(--size);
button {
width: 100%;
height: 100%;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
.icon {
transform: rotate(180deg);
}
}
.dropdown--menu {
position: absolute;
right: 5%;
bottom: 135%;
background: var(--black);
width: 240px;
border-radius: 0.45rem;
transform-origin: 95% 115%;
transform: scale(1);
transition: transform var(--transition-duration-300)
cubic-bezier(0.65, -0.65, 0.25, 1.25),
opacity var(--transition-duration-300)
cubic-bezier(0.65, -0.65, 0.25, 1.25);
&::after {
--size: 12px;
content: "";
position: absolute;
right: calc(0.8 * var(--size));
bottom: calc(-0.5 * var(--size));
transform: rotate(45deg);
width: var(--size);
height: var(--size);
background: var(--black);
}
&.hide {
opacity: 0;
transform: scale(0.5);
}
ul {
padding: 5px 0;
li {
list-style: none;
padding: 10px 0;
width: 100%;
cursor: pointer;
a {
display: block;
text-align: right;
border-radius: 0;
border: 0;
background: none;
font-weight: normal;
font-size: 0.95rem;
color: var(--white);
transition: color var(--transition-duration-100) ease;
span {
color: var(--gray-light);
}
}
&:last-child {
border-top: 1px solid var(----gray-light);
}
&:hover,
&:focus {
a {
color: var(--gray-light);
}
}
}
}
}
}
}
&.g-2 {
button {
padding: 0 8px;
.icon {
padding: 2px;
}
}
}
}
}
.image-info--container {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.65);
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(2px);
padding: 0 2rem;
.image-info--modal {
position: relative;
width: 550px;
border-radius: 0.35rem;
background-position: center;
background-size: cover;
.btn.close {
background: rgb(153, 153, 153);
}
.row {
padding: 18px 25px;
&--1 {
h1 {
margin: 0;
font-weight: 400;
font-size: 1.5rem;
}
span {
font-size: 0.8rem;
}
}
&--2 {
display: flex;
.image-info {
margin-right: 30px;
.info {
display: flex;
align-items: center;
padding: 5px 0;
.icon {
--size: 16px;
width: var(--size);
height: var(--size);
svg {
fill: var(--black);
}
}
&--title {
font-size: 0.8rem;
padding: 0 5px;
font-weight: 500;
}
&--value {
font-weight: bold;
letter-spacing: 1px;
padding-left: 0.4rem;
}
}
.monthly-stats {
font-size: 0.75rem;
color: var(--gray-light);
}
&.likes--container {
.icon {
svg {
stroke: var(--black);
stroke-width: 3px;
fill: none;
}
}
}
}
}
&--3 {
display: grid;
grid-template-columns: repeat(3, minmax(100px, 1fr));
grid-template-rows: repeat(3, 1fr);
grid-gap: 1rem;
.camera-info {
.info--title {
font-size: 0.75rem;
color: var(--gray-dark);
margin-bottom: 6px;
display: block;
}
.info--value {
font-size: 0.85rem;
}
}
}
}
.hr-line {
width: 95%;
height: 1px;
background: var(--gray-light);
opacity: 0.5;
margin: auto;
}
}
}
}
}
.btn.load-more {
border: none;
margin: 5rem auto 20px auto;
background: hsl(0, 0%, 55%);
padding: 10px 20px;
border-radius: 0.5em;
color: var(--white);
text-transform: capitalize;
transition: background var(--transition-duration-300) ease;
box-shadow: 0 6.7px 5.3px rgba(0, 0, 0, 0.016), 0 22.3px 17.9px rgba(0, 0, 0, 0.024),
0 100px 80px rgba(0, 0, 0, 0.04);
&:hover {
background: hsl(0, 0%, 40%);
}
&.loading {
background: hsl(0, 0%, 40%);
cursor: not-allowed;
}
#wave {
position: relative;
width: 100%;
height: 100%;
.dot {
--size: 6px;
display: inline-block;
width: var(--size);
height: var(--size);
border-radius: 50%;
margin-right: 3px;
background: var(--white);
animation: wave 1.2s ease infinite;
&:nth-child(2) {
animation-delay: -1.1s;
}
&:nth-child(3) {
animation-delay: -0.9s;
}
}
}
}
.loading-text {
width: 100%;
text-align: center;
color: var(--gray-dark);
font-size: 2rem;
}
.failed-info-container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
.failed-text,
p {
text-align: center;
color: var(--gray-dark);
}
.failed-text {
font-size: 4rem;
margin-bottom: 0.4rem;
}
p {
text-transform: uppercase;
font-weight: 700;
}
}
.no-image-found-info {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
text-align: center;
color: var(--gray-dark);
h3 {
font-size: 4rem;
margin-bottom: 1rem;
}
}
.text-info {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 15px;
&.type-something-info {
font-size: 1.5rem;
color: var(--gray-light);
}
}
@media only screen and (max-width: 900px) {
.modal {
.image-container {
width: 90%;
img {
width: 100%;
}
}
}
}
@media only screen and (max-width: 550px) {
header {
position: sticky;
background: var(--body-background);
z-index: 30;
top: 0;
padding: 0 1rem;
form {
.form-group {
&.g-1 {
display: none;
}
}
}
}
}
@media only screen and (min-width: 550px) {
header {
form {
.form-group {
&.g-2 {
display: none;
}
}
.floating--container {
display: none;
}
}
}
}
@media only screen and (max-width: 400px) {
.modal {
.image-container .image-info--container .image-info--modal {
.row--3 {
grid-template-columns: repeat(2, 1fr);
}
}
}
}
@keyframes wave {
0%,
60%,
100% {
transform: initial;
}
30% {
transform: translateY(-100%);
}
}
@keyframes hue-rotation {
0% {
filter: hue-rotate(360deg);
}
100% {
filter: hue-rotate(0deg);
}
}
View Compiled
console.clear()
const { useEffect, useState, Fragment, useRef } = React;
const API = 'https://api.unsplash.com';
const CLIENT_ID = '8e31e45f4a0e8959d456ba2914723451b8262337f75bcea2e04ae535491df16d';
const DEFAULT_IMAGE_COUNT = 25;
const IMAGE_INCREMENT_COUNT = 10;
function App() {
const [images, setImages] = useState([]);
const [query, setQuery] = useState("");
const [prevQuery, setPrevQuery] = useState("");
const [clickedImage, setClickedImage] = useState({});
const [isModalActive, setModalActive] = useState(false);
const [failedToLoad, setFailedToLoad] = useState(false);
const [perPageCount, setPerPageCount] = useState(DEFAULT_IMAGE_COUNT);
const [totalResults, setTotalResults] = useState(0);
const [state, setState] = useState("loading");
const [queryChanged, setqueryChanged] = useState(false);
const [page, setPage] = useState("home");
const loadButtonEl = useRef();
const getRandomPhotos = () => {
setState("loading");
axios(`${API}/photos/random`, {
params: {
count: DEFAULT_IMAGE_COUNT,
client_id: CLIENT_ID,
},
})
.then((res) => {
setImages(res.data);
if (failedToLoad) setFailedToLoad(false);
})
.catch(() => setFailedToLoad(true));
};
useEffect(getRandomPhotos, [failedToLoad]);
useEffect(waitForImages, [images]);
const searchQuery = () => {
if (query !== "") {
setState("loading");
setPage("search");
axios(`${API}/search/photos/`, {
params: {
query: query,
per_page: perPageCount,
client_id: CLIENT_ID,
},
})
.then((res) => {
setImages(res.data.results);
setTotalResults(res.data.total);
if (failedToLoad) setFailedToLoad(false);
})
.catch(() => setFailedToLoad(true));
}
};
useEffect(searchQuery, [perPageCount]);
useEffect(() => {
if (page === "home") {
setQuery("");
setPrevQuery("");
}
}, [page]);
useEffect(() => {
let loaded = 0;
let cards = document.getElementsByClassName("image-card");
for (let i = 0; i < cards.length; i++) {
imagesLoaded(cards[i], (instance) => {
if (instance.isComplete) loaded++;
if (loaded === perPageCount) setState("loaded");
});
}
}, [images, perPageCount]);
const handleSearch = () => {
if (query !== prevQuery && query !== "") {
setPerPageCount(DEFAULT_IMAGE_COUNT);
setPrevQuery(query);
searchQuery();
}
};
const handleImageClick = (img) => {
setClickedImage(img);
setModalActive(true);
};
return (
<Fragment>
<Header
onQueryChange={(q) => {
setQuery(q);
setqueryChanged(true);
}}
onQuerySearch={handleSearch}
onGenarateRandomImages={() => getRandomPhotos()}
onPageChange={(p) => setPage(p)}
/>
{prevQuery !== "" && page !== "home" && (
<span className="text-info">
<span>
search results for <strong>"{prevQuery}"</strong>
</span>
<span className="total-results">
found <strong>{totalResults}</strong> matching results
</span>
</span>
)}
{query === "" && queryChanged && !failedToLoad && (
<h3 className="text-info type-something-info">Type something!</h3>
)}
{prevQuery !== "" && totalResults === 0 && (
<div className="no-image-found-info">
<h3>No Images Found</h3>
<span>
Try searching <strong>dogs</strong> or <strong>cats</strong>
</span>
</div>
)}
{(!failedToLoad && (
<main>
<ImageCardList
images={images}
onImageClicked={(img) => handleImageClick(img)}
/>
<h3
className="text-info loading-text"
style={{
display: state === "loading" && !queryChanged ? "block" : "none",
}}
>
Loading...
</h3>
<button
className={`btn load-more ${state === "loading" ? "loading" : ""}`}
ref={loadButtonEl}
onClick={() => {
setPerPageCount(perPageCount + IMAGE_INCREMENT_COUNT);
setState("loading");
}}
style={{
display:
perPageCount >= totalResults || page === "home" ? "none" : "block",
pointerEvents: state === "loading" ? "none" : "all",
}}
>
{(state !== "loading" && "load more") || (
<div id="wave">
<span className="dot"></span>
<span className="dot"></span>
<span className="dot"></span>
</div>
)}
</button>
</main>
)) ||
(failedToLoad && (
<div className="failed-info-container">
<h1 className="failed-text">Failed To Load</h1>
<p>check your connection</p>
</div>
))}
{isModalActive && (
<Modal
imageData={clickedImage}
onModalActive={(isActive) => setModalActive(isActive)}
/>
)}
</Fragment>
);
}
function resizeMasonryItem(item) {
let grid = document.getElementsByClassName("masonry")[0],
rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue("grid-row-gap")),
rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue("grid-auto-rows"));
let rowSpan = Math.ceil(
(item.querySelector(".masonry-content").getBoundingClientRect().height + rowGap) /
(rowHeight + rowGap)
);
item.style.gridRowEnd = "span " + rowSpan;
}
function resizeAllMasonryItems() {
let allItems = document.getElementsByClassName("masonry-brick");
for (let i = 0; i < allItems.length; i++) {
resizeMasonryItem(allItems[i]);
}
}
function waitForImages() {
let allItems = document.getElementsByClassName("masonry-brick");
for (let i = 0; i < allItems.length; i++) {
imagesLoaded(allItems[i], (instance) => {
const item = instance.elements[0];
const cardForegroundEl = instance.images[0].img.parentElement.parentElement.querySelector(
".image-card-fg"
);
item.style.display = "block";
let t = setTimeout(() => {
cardForegroundEl.classList.add("hide");
clearTimeout(t);
}, 200 + +cardForegroundEl.parentElement.getAttribute("data-card-index") * 120);
resizeMasonryItem(item);
});
}
}
const events = ["load", "resize"];
events.forEach((event) => {
window.addEventListener(event, resizeAllMasonryItems);
});
const Header = ({ onQueryChange, onQuerySearch, onGenarateRandomImages, onPageChange }) => {
const [isFloatingInputActive, setFloatingInputActive] = useState(false);
const inputEl = useRef();
const floatingInputEl = useRef();
const handleSubmit = (e) => {
e.preventDefault();
};
const handleHomePage = () => {
onPageChange("home");
onGenarateRandomImages();
inputEl.current.value = "";
floatingInputEl.current.value = "";
};
return (
<header>
<button className="btn home" onClick={handleHomePage}>
<div className="icon gallery">
<svg viewBox="0 0 388.309 388.309" xmlns="http://www.w3.org/2000/svg">
<path
d="M201.173,307.042c-11.291-0.174-22.177-4.233-30.825-11.494l-56.947-50.155c-8.961-8.373-22.664-9.036-32.392-1.567
L0.03,303.384v38.661c-0.582,10.371,7.355,19.25,17.726,19.832c0.534,0.03,1.07,0.037,1.605,0.021h286.824
c11.494,0,22.988-8.359,22.988-19.853V240.691L226.25,300.25C218.684,304.798,210.001,307.15,201.173,307.042z"
/>
<circle cx="196.993" cy="182.699" r="22.988" />
<path
d="M383.508,67.238c-3.335-4.544-8.487-7.406-14.106-7.837L84.667,26.487c-5.551-0.465-11.091,1.012-15.673,4.18
c-4.058,3.524-6.817,8.307-7.837,13.584l-4.702,40.751h249.731c23.809,0.54,43.061,19.562,43.886,43.363v184.424
c0-1.045,4.702-2.09,6.792-4.18c4.326-3.397,6.834-8.606,6.792-14.106L388.21,82.389
C388.753,76.91,387.057,71.445,383.508,67.238z"
/>
<path
d="M306.185,105.899H19.361c-11.494,0-19.331,10.971-19.331,22.465v148.898l68.963-50.155
c17.506-12.986,41.724-11.895,57.992,2.612l57.469,50.155c8.666,7.357,21.044,8.406,30.824,2.612l113.894-66.351v-87.771
C328.382,116.099,318.465,106.408,306.185,105.899z M196.993,226.584c-24.237,0-43.886-19.648-43.886-43.886
c0-24.237,19.648-43.886,43.886-43.886c24.237,0,43.886,19.648,43.886,43.886C240.879,206.936,221.231,226.584,196.993,226.584z"
/>
</svg>
</div>
<span className="title">Gallery</span>
</button>
<form onSubmit={handleSubmit}>
<div className="form-group g-1">
<input
type="text"
placeholder="Type here"
onChange={() => onQueryChange(inputEl.current.value)}
ref={inputEl}
/>
<button
className="btn search"
onClick={onQuerySearch}
disabled={
inputEl.current !== undefined && inputEl.current.value.length === 0
? true
: false
}
>
<div className="icon search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"></path>
</svg>
</div>
<span>search</span>
</button>
</div>
<div className="form-group g-2">
<button className="btn search" onClick={() => setFloatingInputActive(true)}>
<div className="icon search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"></path>
</svg>
</div>
</button>
</div>
<div className={`floating--container ${isFloatingInputActive ? "" : "hide"}`}>
<div className="input--container">
<input
type="text"
placeholder="Type here"
onChange={() => onQueryChange(floatingInputEl.current.value)}
ref={floatingInputEl}
/>
<button
className="btn search"
onClick={onQuerySearch}
disabled={
floatingInputEl.current !== undefined &&
floatingInputEl.current.value.length === 0
? true
: false
}
>
search
</button>
</div>
<button className="btn close" onClick={() => setFloatingInputActive(false)}>
<div className="icon cross">
<svg
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="false"
>
<path d="M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4 L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1 c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1 c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z" />
</svg>
</div>
</button>
</div>
</form>
</header>
);
};
const ImageCard = ({ imageData, imageIndex, onImageClicked }) => {
const { urls, alt_description, user, links, color } = imageData;
return (
<div className="masonry-brick" style={{ display: "none" }}>
<div
className="image-card masonry-content"
style={{
background: color,
}}
data-card-index={`${imageIndex}`}
>
<img className="image" src={urls.regular} alt={alt_description} />
<div
className="image-card--clickable-area"
onClick={() => {
onImageClicked(imageData);
document.title = alt_description;
}}
/>
<div className="image-card--options">
<div className="user-info">
<a href={user.links.html} target="_blank_" tabIndex="-1">
<img src={user.profile_image.medium} alt={user.name} />
</a>
<h3>{user.name}</h3>
</div>
<div className="links">
<a
className="link"
href={`https://instagram.com/${user.instagram_username}`}
style={{
display: user.instagram_username ? "block" : "none",
}}
target="_blank_"
tabIndex="-1"
>
<div className="icon instagram">
<svg
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="false"
>
<path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"></path>
</svg>
</div>
</a>
<a
className="link"
href={`https://twitter.com/${user.twitter_username}`}
style={{
display: user.twitter_username ? "block" : "none",
}}
target="_blank_"
tabIndex="-1"
>
<div className="icon twitter">
<svg
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="false"
>
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path>
</svg>
</div>
</a>
<a className="link" href={`${links.download}?force=true`} tabIndex="-1">
<div className="icon download">
<svg
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="false"
>
<path d="M25.8 15.5l-7.8 7.2v-20.7h-4v20.7l-7.8-7.2-2.7 3 12.5 11.4 12.5-11.4z"></path>
</svg>
</div>
</a>
</div>
</div>
<div
className="image-card-fg"
style={{
background: color,
}}
/>
</div>
</div>
);
};
const ImageCardList = ({ images, onImageClicked }) => {
return (
<div className="masonry">
{images.map((image, index) => (
<ImageCard
imageData={image}
key={image.id}
imageIndex={index}
onImageClicked={(img) => onImageClicked(img)}
/>
))}
</div>
);
};
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const Modal = ({ imageData, onModalActive }) => {
const { urls, alt_description, color, links, height, width, created_at, exif } = imageData;
const date = new Date(created_at);
const [dropdownActive, setDropdownActive] = useState(false);
const [infoActive, setInfoActive] = useState(false);
const [views, setViews] = useState(0);
const [downloads, setDownloads] = useState(0);
const [likes, setLikes] = useState(0);
const [viewsLastMonth, setViewsLastMonth] = useState(0);
const [downloadsLastMonth, setDownloadsLastMonth] = useState(0);
const [likesLastMonth, setLikesLastMonth] = useState(0);
const modalEl = useRef();
const imageEl = useRef();
const handleClose = () => {
modalEl.current.style.display = "none";
onModalActive(false);
document.title = "Image Gallery | UNSPLASH";
};
const handleDropdown = () => {
setDropdownActive(!dropdownActive);
};
const fetchStats = () => {
axios(`${API}/photos/${imageData.id}/statistics`, {
params: {
client_id: CLIENT_ID,
},
}).then((res) => {
const lastMonthViewValues = res.data.views.historical.values.map((e) => e.value);
const lastMonthDownloadValues = res.data.downloads.historical.values.map(
(e) => e.value
);
const lastMonthLikeValues = res.data.likes.historical.values.map((e) => e.value);
const lastMonthTotalViews = numberFormatter(returnTotal(lastMonthViewValues), 1);
const lastMonthTotalDownloads = numberFormatter(
returnTotal(lastMonthDownloadValues),
1
);
const lastMonthTotalLikes = numberFormatter(returnTotal(lastMonthLikeValues), 1);
setViews(res.data.views.total);
setDownloads(res.data.downloads.total);
setLikes(res.data.likes.total);
setViewsLastMonth(lastMonthTotalViews);
setDownloadsLastMonth(lastMonthTotalDownloads);
setLikesLastMonth(lastMonthTotalLikes);
});
};
useEffect(fetchStats, []);
return (
<div className="modal" ref={modalEl}>
<div
className="image-container"
style={{
background: color,
height: width / height > 2 ? "50%" : "80%",
}}
>
<img src={urls.full} alt={alt_description} ref={imageEl} />
<button className="btn close" onClick={handleClose}>
<div className="icon">
<svg
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="false"
>
<path d="M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4 L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1 c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1 c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z" />
</svg>
</div>
</button>
<div className="options--container">
<div className="group g-1">
<a href={`${links.download}?force=true`} className="download">
download
</a>
<div className="dropdown-btn--container">
<button className="btn dropdown" onClick={handleDropdown}>
<div className="icon">
<svg viewBox="0 0 32 32" aria-hidden="false">
<path d="M9.9 11.5l6.1 6.1 6.1-6.1 1.9 1.9-8 8-8-8 1.9-1.9z"></path>
</svg>
</div>
</button>
<div className={`dropdown--menu ${dropdownActive ? "" : "hide"}`} tabIndex="-1">
<ul>
<li>
<a href={`${links.download}?force=true&w=640`}>
Small <span>(640x960)</span>
</a>
</li>
<li>
<a href={`${links.download}?force=true&w=1920`}>
Medium <span>(1920x2880)</span>
</a>
</li>
<li>
<a href={`${links.download}?force=true&w=2400`}>
Large <span>(2400x3600)</span>
</a>
</li>
<li>
<a href={`${links.download}?force=true`}>
Original Size{" "}
<span>
({height}x{width})
</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div className="group g-2">
<button className="btn info" onClick={() => setInfoActive(true)}>
<div className="icon">
<svg
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"></path>
</svg>
</div>
</button>
</div>
</div>
<div
className="image-info--container"
style={{
display: infoActive ? "flex" : "none",
}}
>
<div
className="image-info--modal"
style={{
backgroundImage: `linear-gradient(rgba(255, 255, 255, 0.8), white 150px), url("${urls.thumb}&auto=format&fit=crop&w=200&q=60&blur=20")`,
}}
>
<button className="btn close" onClick={() => setInfoActive(false)}>
<div className="icon">
<svg
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="false"
>
<path d="M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4 L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1 c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1 c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z" />
</svg>
</div>
</button>
<div className="row row--1">
<h1>Info</h1>
<span>
Published on {months[date.getMonth()]} {date.getDate()},{" "}
{date.getFullYear()}
</span>
</div>
<div className="row row--2">
<div className="image-info views--container">
<div className="info">
<div className="icon">
<svg
version="1.1"
viewBox="0 0 32 32"
width="32"
height="32"
aria-hidden="false"
>
<path d="M31.8 15.1l-.2-.4c-3.5-6.9-9.7-11.5-15.6-11.5-6.3 0-12.3 4.5-15.7 11.7l-.2.4c-.2.5-.2 1.1 0 1.6l.2.4c3.6 7 9.7 11.5 15.6 11.5 6.3 0 12.3-4.5 15.6-11.6l.2-.4c.4-.5.4-1.2.1-1.7zm-2 1.2c-3 6.5-8.3 10.5-13.8 10.5-5.2 0-10.6-4.1-13.8-10.4l-.2-.4.1-.3c3.1-6.5 8.4-10.5 13.9-10.5 5.2 0 10.6 4.1 13.8 10.4l.2.4-.2.3zm-13.8-6.6c-3.5 0-6.3 2.8-6.3 6.3s2.8 6.3 6.3 6.3 6.3-2.8 6.3-6.3-2.8-6.3-6.3-6.3zm0 10.6c-2.4 0-4.3-1.9-4.3-4.3s1.9-4.3 4.3-4.3 4.3 1.9 4.3 4.3-1.9 4.3-4.3 4.3z"></path>
</svg>
</div>
<span className="info--title">Views</span>
</div>
<div className="info info--value">
<span className="total-views">{beautifyNumber(views)}</span>
</div>
<div className="monthly-stats">
<span className="views-stat">+{viewsLastMonth}</span> since last
month
</div>
</div>
<div className="image-info downloads--container">
<div className="info">
<div className="icon">
<svg
version="1.1"
viewBox="0 0 32 32"
width="32"
height="32"
aria-hidden="false"
>
<path d="M27.5 18.08a1 1 0 0 0-1.42 0l-9.08 8.59v-23.67a1 1 0 0 0-2 0v23.67l-9.08-8.62a1 1 0 1 0-1.38 1.45l10.77 10.23a1.19 1.19 0 0 0 .15.09.54.54 0 0 0 .16.1.94.94 0 0 0 .76 0 .54.54 0 0 0 .16-.1l.15-.09 10.77-10.23a1 1 0 0 0 .04-1.42z"></path>
</svg>
</div>
<span className="info info--title">Downloads</span>
</div>
<div className="info info--value">
<span className="total-views">{beautifyNumber(downloads)}</span>
</div>
<div className="monthly-stats">
<span className="downloads-stat">+{downloadsLastMonth}</span>{" "}
since last month
</div>
</div>
<div className="image-info likes--container">
<div className="info">
<div className="icon">
<svg
version="1.1"
viewBox="0 -4 32 40"
width="32"
height="32"
aria-hidden="false"
>
<path d="M17.4 29c-.8.8-2 .8-2.8 0l-12.3-12.8c-3.1-3.1-3.1-8.2 0-11.4 3.1-3.1 8.2-3.1 11.3 0l2.4 2.8 2.3-2.8c3.1-3.1 8.2-3.1 11.3 0 3.1 3.1 3.1 8.2 0 11.4l-12.2 12.8z"></path>
</svg>
</div>
<span className="info info--title">Likes</span>
</div>
<div className="info info--value">
<span className="total-views">{beautifyNumber(likes)}</span>
</div>
<div className="monthly-stats">
<span className="likes-stat">+{likesLastMonth}</span> since last
month
</div>
</div>
</div>
<div className="hr-line"></div>
<div className="row row--3">
<div className="camera-info">
<span className="info--title">Camera Make</span>
<span className="info--value">
{exif !== undefined ? exif.make : "--"}
</span>
</div>
<div className="camera-info">
<span className="info--title">Camera Model</span>
<span className="info--value">
{exif !== undefined ? exif.model : "--"}
</span>
</div>
<div className="camera-info">
<span className="info--title">Focal Length</span>
<span className="info--value">
{exif !== undefined ? `${exif.focal_length}mm` : "--"}
</span>
</div>
<div className="camera-info">
<span className="info--title">Aperture</span>
<span className="info--value">
{exif !== undefined ? `ƒ/${exif.aperture}` : "--"}
</span>
</div>
<div className="camera-info">
<span className="info--title">Shutter Speed</span>
<span className="info--value">
{exif !== undefined ? `${exif.exposure_time}s` : "--"}
</span>
</div>
<div className="camera-info">
<span className="info--title">ISO</span>
<span className="info--value">
{exif !== undefined ? exif.iso : "--"}
</span>
</div>
<div className="camera-info">
<span className="info--title">Dimensions</span>
<span className="info--value">
{`${width} × ${height}` || "--"}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
function numberFormatter(num, digits) {
let si = [
{ value: 1, symbol: "" },
{ value: 1e3, symbol: "k" },
{ value: 1e6, symbol: "M" },
{ value: 1e9, symbol: "G" },
{ value: 1e12, symbol: "T" },
{ value: 1e15, symbol: "P" },
{ value: 1e18, symbol: "E" },
];
let rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
let i;
for (i = si.length - 1; i > 0; i--) {
if (num >= si[i].value) {
break;
}
}
return (num / si[i].value).toFixed(digits).replace(rx, "$1") + si[i].symbol;
}
function beautifyNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function returnTotal(arrOfValues) {
return arrOfValues.reduce((a, c) => (a += c));
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
View Compiled