Skip to content

Instantly share code, notes, and snippets.

@mqxu
Created March 14, 2021 17:08
Show Gist options
  • Save mqxu/8258aa82f3a60eaf8cb6881bd3590bd3 to your computer and use it in GitHub Desktop.
Save mqxu/8258aa82f3a60eaf8cb6881bd3590bd3 to your computer and use it in GitHub Desktop.
Reading Application
<div class="container">
<div class="status-bar">
<span class="status-bar__clock" onload="showTime()"></span>
<div>
<img class="status-bar__img img" src="http://utopian-drink.surge.sh/images/icon/status-bar.svg" alt="status-bar">
</div>
</div>
<header class="header">
<div class="header__context">
<h1 class="header__title">browse</h1>
<b class="header__text">recommended</b>
</div>
<nav class="tabbar">
<ul class="tabbar__list list">
<li class="tabbar__item">history</li>
<li class="tabbar__item">classical</li>
<li class="tabbar__item active">biography</li>
<li class="tabbar__item">cartoon</li>
<li class="tabbar__item">fantasy</li>
<li class="tabbar__item">drama</li>
<li class="tabbar__item">memoir</li>
<li class="tabbar__item">self help</li>
</ul>
</nav>
</header>
<div class="global-button global-button--position-ab flex">
<button class="global-button__back">
<img class="img" src="http://utopian-drink.surge.sh/images/icon/back.svg" alt="icon-back">
</button>
<button>
<img class="img" src="http://utopian-drink.surge.sh/images/icon/menu.svg" alt="icon-menu">
</button>
</div>
<main class="main">
<ul class="product-card list"></ul>
</main>
</div>
// Designed by: Crank
// Original image: https://dribbble.com/shots/5707503-Reading-Application?utm_source=Pinterest_Shot&utm_campaign=crankwh&utm_content=Reading%20Application&utm_medium=Social_Share
"use strict";
gsap.registerPlugin(ScrollTrigger);
let info ;
let genre;
const request = "https://www.json-generator.com/api/json/get/cqyGXnDfjC?indent=2";
const html = document.documentElement;
const container = document.querySelector(".container")
const tabbarList = document.querySelector(".tabbar__list");
const main = container.querySelector(".main")
const productCard = main.querySelector(".product-card");
let productCardItems;
const header = document.querySelector(".header");
const time = parseFloat( getComputedStyle(document.documentElement).getPropertyValue("--duration-header") ) * 1000;
const globalButton = document.querySelector(".global-button");
const globalButtonBack = globalButton.querySelector(".global-button__back");
let elements;
let activeItem;
let isMove = false;
let startY;
let scrollTop;
let startX;
let scrollLeft;
let tabbarItemActive = tabbarList.querySelector(".tabbar__item.active");
const statusBar = document.querySelector(".status-bar");
const statusBarClock = document.querySelector(".status-bar__clock");
let progressScroll = 0;
let notScroll = false;
fetch(request)
.then(response => response.json())
.then(item => {
info = item;
booksGenre(info);
setMaxHeight();
})
function setMaxHeight() {
document.body.style.height = window.innerHeight + "px";
const maxHeightItem = container.clientHeight - globalButton.offsetHeight - statusBar.offsetHeight;
html.style.setProperty("--max-height-item", `${maxHeightItem}px`);
html.style.setProperty("--height-body", `${window.innerHeight}px`);
}
function booksGenre(info){
const key = tabbarItemActive.textContent;
genre = info[key];
if (!genre) return;
productCard.innerHTML = "";
genre.forEach(book => {
book.rank = `${book.rank}`.replace(/^\d$/, "$&.0");
const width = 100 - book.rank * 20;
book.keywords.forEach(word => {
const regex = new RegExp(`${word}`, `i`);
book.description = book.description.replace(regex, `<b style="color:black">${word}</b>`);
})
productCard.insertAdjacentHTML("beforeend", creatBook(book, width));
})
productCardItems = Array.from(productCard.children);
const productCardHeight = (genre.length + 3) * productCard.firstElementChild.offsetHeight;
html.style.setProperty("--productCardHeight", `${productCardHeight}px`);
main.scrollTop = 0;
ScrollTrigger.getAll().forEach(item => {
item.kill()
});
roll(productCardItems);
}
function creatBook(book, width) {
const {picture, name, author, rank, view, description} = book;
return `
<li class="product-card__item">
<div class="product-card__img">
<img class="img" src="${picture}" alt="book">
</div>
<article class="product-card__content">
<header class="product-card__info">
<h2 class="product-card__product-name">${name}</h2>
<footer class="product-card__product-author">${author}</footer>
<div class="product-card__rank">
<div class="product-card__starts flex" style="--rank:${width}%;">
<img class="product-card__start-icon" src="http://utopian-drink.surge.sh/images/icon/star.svg" alt="star-icon">
<img class="product-card__start-icon" src="http://utopian-drink.surge.sh/images/icon/star.svg" alt="star-icon">
<img class="product-card__start-icon" src="http://utopian-drink.surge.sh/images/icon/star.svg" alt="star-icon">
<img class="product-card__start-icon" src="http://utopian-drink.surge.sh/images/icon/star.svg" alt="star-icon">
<img class="product-card__start-icon" src="http://utopian-drink.surge.sh/images/icon/star.svg" alt="star-icon">
</div>
<span class="product-card__rank-number">${rank}</span>
<span class="product-card__view-value hide">(${view})</span>
</div>
<div class="product-card__view">
<p class="product-card__view-number">${view}</p>
<p>views</p>
</div>
</header>
<div class="flex product-card__buttons">
<button class="flex">
<img class="product-card__button-icon img" src="http://utopian-drink.surge.sh/images/icon/reviews.svg" alt="reviews">
<p class="product-card__button-text">see reviews</p>
</button>
<button class="flex">
<img class="product-card__button-icon img" src="http://utopian-drink.surge.sh/images/icon/heart.svg" alt="like">
<p class="product-card__button-text">like</p>
</button>
<button class="flex">
<img class="product-card__button-icon img" src="http://utopian-drink.surge.sh/images/icon/share.svg" alt="share">
<p class="product-card__button-text">share</p>
</button>
</div>
<footer class="product-card__context">
<h3>about the book</h3>
<p class="product-card__description">${description}</p>
</footer>
</article>
<button class="product-card__cta">read now</button>
</li>`
}
function openProductCard(e, productCardItems) {
const target = e.target;
activeItem = target.closest(".product-card__item");
if ( target.closest(".product-card__img")) {
activeItem.classList.remove("active");
closeProductCard(elements);
return;
}
if ( !target.closest(".product-card__content") ) return;
const indexActiveItem = productCardItems.findIndex(item => item == activeItem);
notScroll = true;
main.scrollTo(0, indexActiveItem * productCardItems[0].offsetHeight);
const productCardWrapperImg = activeItem.querySelector(".product-card__img");
const productCardContent = activeItem.querySelector(".product-card__content");
const productCardView = productCardContent.querySelector(".product-card__view");
const productCardViewValue = productCardContent.querySelector(".product-card__view-value");
elements = [
activeItem,
productCardWrapperImg,
productCardContent,
productCard,
header,
container,
productCardView,
productCardViewValue,
];
header.classList.add("hide");
if (header.classList.contains("hide")) {
productCard.style.paddingTop = `${globalButton.offsetHeight}px`;
}
activeItem.style.cssText = `
--timeOut : 0s;
--transform : none;
`
main.style.cssText = `
pointer-events : none;
overflow: hidden;
`;
container.classList.add("active");
productCardView.classList.add("hide") ;
productCardViewValue.classList.remove("hide");
productCardContent.classList.add("open-content");
productCardWrapperImg.classList.add("move-img");
activeItem.classList.add("active");
}
function closeProductCard(elements) {
const [
activeItem,
productCardWrapperImg,
productCardContent,
productCard,
header,
container,
productCardView,
productCardViewValue,
] = elements;
if (!activeItem.classList.contains("active")) {
productCardWrapperImg.classList.add("back-img");
productCardContent.classList.remove("open-content");
container.classList.remove("active");
productCardView.classList.remove("hide");
productCardViewValue.classList.add("hide");
productCardContent.style.opacity = 0;
setTimeout(() => {
productCard.style = "";
header.classList.remove("hide");
},time);
}
productCardWrapperImg.addEventListener("animationend", function(e) {
if(e.animationName.includes("img-back")) {
productCardItems.forEach(item => {
item.style.removeProperty("--timeOut");
item.classList.remove("hide");
});
this.classList.remove("move-img", "back-img");
productCardContent.style = "";
main.style = "";
activeItem.style = "";
}
});
}
function showTime() {
const date = new Date();
let hours = date.getHours();
let minutes = date.getMinutes();
if (hours == 0) {
hours = 12;
}
if (hours > 12) {
hours = hours - 12;
}
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
statusBarClock.textContent = `${hours}:${minutes}`;
setTimeout(showTime, 1000);
};
showTime();
tabbarList.addEventListener("click", function(e) {
if (tabbarItemActive == e.target) return;
if (e.target.tagName != "LI") return;
tabbarItemActive = e.target;
booksGenre(info);
if (!genre) return;
tabbarItemActive.classList.add("active");
gsap.to(".product-card__content", {
duration: .4,
rotationY : -180,
scaleX: -1,
});
gsap.fromTo(".product-card__img",
{duration : .75 ,filter: "brightness(.5)"},
{duration : .75 ,filter: "brightness(1)"});
gsap.to(".product-card__content", {clearProps:"all"});
tabbarList.querySelectorAll(".tabbar__item").forEach(item => {
if (item != tabbarItemActive) {
item.classList.remove("active");
}
});
});
tabbarList.addEventListener("pointerdown", function(e) {
isMove = true;
startX = Math.floor(e.pageX - this.getBoundingClientRect().left);
scrollLeft = this.scrollLeft;
});
tabbarList.addEventListener("pointermove", function(e) {
if (!isMove) return;
this.style.cssText = `
--pointer-event: none;
cursor : grabbing;
`
const lastX = Math.floor(e.pageX - this.getBoundingClientRect().left);
const walk = lastX - startX;
this.scrollLeft = scrollLeft - walk;
});
tabbarList.addEventListener("pointerup", function() {
isMove = false;
this.style = "";
});
tabbarList.addEventListener("pointerleave", function() {
isMove = false;
this.style = "";
});
header.addEventListener("transitionend", function() {
if (this.classList.contains("hide")) {
globalButton.classList.add("show");
}
});
header.addEventListener("transitionstart", () => {
globalButton.classList.remove("show");
});
globalButtonBack.addEventListener("click", () => {
activeItem.classList.remove("active");
closeProductCard(elements);
});
productCard.addEventListener("click", (e) => openProductCard(e, productCardItems) );
function roll(productCardItems) {
productCardItems.forEach((item, index) => {
gsap.set(item ,{transformPerspective : 1000,});
const animation = gsap.to(item, {
rotationX : 95,
y : 70,
opacity : 0,
});
ScrollTrigger.create({
animation: animation,
trigger : item,
scroller : main,
start : `top+=5 top`,
end : `bottom-=5 top`,
scrub : true,
onEnter : progress => {
progressScroll = progress.end + 5;
},
onLeaveBack : progress => {
progressScroll = progress.start - 5;
},
onEnterBack : progress => {
progressScroll = progress.start - 5;
},
onUpdate: ({direction ,isActive}) => {
productCardItems.forEach(item => {
if (index == productCardItems.length - 1) return;
if (isActive) item.style.marginBottom = direction * .25 + "em";
else item.style.removeProperty("margin-bottom");
});
},
});
})
}
function scrollInMain() {
if (notScroll) return
main.scroll({
top : progressScroll,
behavior : "smooth"
});
}
main.addEventListener("pointerdown", function(e) {
if (activeItem && activeItem.classList.contains("active")) return;
isMove = true;
startY = Math.floor(e.pageY - this.getBoundingClientRect().top);
scrollTop = this.scrollTop;
});
main.addEventListener("pointermove", function(e) {
if (!isMove) return;
notScroll = false;
const lastY = Math.floor(e.pageY - this.getBoundingClientRect().top);
const walk = lastY - startY;
this.scrollTop = scrollTop - walk;
this.style.setProperty("--pointer-event", "none");
});
main.addEventListener("pointerup", function() {
isMove = false;
this.style.removeProperty("--pointer-event");
scrollInMain();
});
main.addEventListener("pointerleave", function() {
isMove = false;
this.style.removeProperty("--pointer-event");
});
window.addEventListener("resize", setMaxHeight);
document.ondragstart = () => {
return false;
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.0/ScrollTrigger.min.js"></script>
@import url('https://fonts.googleapis.com/css2?family=Kumbh+Sans&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Rubik&display=swap');
html {
box-sizing: border-box ;
--padding-left: 1.25em ;
--duration-header: .25s ;
--duration-product-item: .5s ;
}
html *,
html *::before,
html *::after {
box-sizing: inherit ;
scrollbar-width: none ;
}
@media (max-width: 26.875em) {
html {
--none: none ;
--top-start: 4.99em ;
--top-end: 2.08em ;
}
}
body{
margin: 0 ;
display: flex ;
user-select: none ;
align-items: center ;
justify-content: center ;
background-color: #dadfea ;
font-family: 'Kumbh Sans', sans-serif ;
-webkit-tap-highlight-color: transparent ;
}
::-webkit-scrollbar{
width: 0 ;
height: 0 ;
}
.list {
margin: 0 ;
padding: 0 ;
list-style-type: none ;
}
.img {
width: 100% ;
display: block ;
object-fit: cover ;
position: relative ;
}
.img::after{
top: 0 ;
left: 0 ;
content: "" ;
width: 100% ;
height: 100% ;
position: absolute ;
background-color: white ;
}
.flex {
display: flex ;
align-items: center ;
justify-content: space-between ;
}
button {
all: unset ;
font: inherit ;
cursor: pointer ;
}
.container {
width: 100% ;
height: 100% ;
display: flex ;
overflow: hidden ;
font-size: 6.4vw ;
position: relative ;
flex-direction: column ;
background-color: white ;
border-radius: var(--none, 1.3em ) ;
border: var(--none, 0.45em solid black) ;
}
.container::before {
right: 0 ;
bottom: 0 ;
opacity: 0 ;
content: " " ;
position: absolute ;
pointer-events: none ;
transition: opacity .2s ;
height: calc(100% - 4.81em) ;
background-color: #f8f9fc ;
width: calc(100% - var(--padding-left)) ;
}
@media (min-width: 26.875em) {
.container {
font-size: 1em ;
width: 15.0375em ;
height: 31.84em ;
}
.container::before {
height: 80.2% ;
}
}
.container.active::before{
opacity: 1 ;
z-index: 100 ;
}
.status-bar {
display: var(--none, flex) ;
padding: .3em .25em 0 .95em ;
justify-content: space-between ;
}
.status-bar__clock {
font-size: .53em ;
font-weight: bold ;
letter-spacing: .075em ;
font-family: 'Rubik', sans-serif ;
}
.status-bar__img {
width: 2.85em ;
}
.header {
padding: 1.15em 0 .25em var(--padding-left) ;
transition: height, var(--duration-header), padding var(--duration-header), transform var(--duration-header), opacity var(--duration-header);
}
.header.hide {
height: 0 ;
opacity: 0 ;
padding-top: 0 ;
padding-bottom: 0 ;
pointer-events: none ;
transform: translate3d(0, -5%, 0) ;
}
.header__context {
display: flex ;
font-size: .6em ;
align-items: baseline ;
text-transform: capitalize ;
}
.header__title {
margin: 0 .8em 0 0 ;
letter-spacing: 0.03em ;
}
.header__text {
color: #414141b4 ;
}
.tabbar {
overflow: hidden ;
}
.tabbar__list {
overflow: auto ;
cursor: pointer ;
white-space: nowrap ;
padding: .6em 1.2em .6em 0 ;
}
.tabbar__item {
font-size: .41em ;
color: #9e9fa2 ;
border-radius: 2em ;
display: inline-flex ;
justify-content: center ;
padding: .75em 1.2em .4em ;
text-transform: capitalize ;
background-color: #f0f1f4 ;
pointer-events: var(--pointer-event) ;
}
.tabbar__item:not(:first-of-type) {
margin-left: .5em ;
}
.tabbar__item.active {
color: #f1f1f1 ;
background-color: #1469fc ;
}
.global-button {
opacity: 0 ;
width: 100% ;
pointer-events: none ;
padding: .55em 1em 0 .8em ;
}
.global-button--position-ab {
top: .5% ;
position: absolute ;
}
@media (min-width: 26.875em) {
.global-button--position-ab {
top: 4.6% ;
}
}
.global-button > * {
width: 1em ;
height: 1em ;
}
.global-button.show {
opacity: 1 ;
pointer-events: initial ;
}
.main {
height: 100% ;
font-size: 1.1em ;
touch-action: none ;
overflow: hidden auto ;
overscroll-behavior: contain ;
}
@media (min-width: 26.875em) {
.main {
font-size: 1em ;
}
}
.product-card {
height: var(--productCardHeight) ;
padding-left: var(--padding-left) ;
}
.product-card__item {
width: 100% ;
display: flex ;
height: 9.92em ;
align-items: center ;
max-height: var(--max-height-item) ;
pointer-events: var(--pointer-event) ;
transform: var(--transform, perspective(1000px) rotateX(0)) ;
transition: height var(--duration-product-item), margin var(--timeOut, calc( var(--duration-product-item) / 1.5 )) ;
}
.product-card__img {
width: 6.55em ;
height: 8.75em ;
overflow: hidden ;
position: absolute ;
border-radius: .3em ;
pointer-events: none ;
transform: translate3d(80%, 0, 0) ;
transition: border-radius var(--duration-product-item) ;
box-shadow: 0.3125em 0.3125em 0.875em -0.1875em #403e3e4f ;
}
.product-card__item.active {
height: 100% ;
padding-top: 3em ;
align-items: unset ;
margin-bottom: 1.5em ;
flex-direction: column ;
}
@media (min-width: 26.875em) {
.product-card__item.active {
padding-top: 3.55em ;
}
}
.product-card__item.active .product-card__img {
z-index: 200 ;
cursor: pointer ;
pointer-events: initial ;
}
.product-card__img.move-img {
border-radius: 0 .3em .3em 0 ;
animation: product-card-img-move var(--duration-product-item) forwards ;
}
@keyframes product-card-img-move {
0%, 20%{
top: var(--top-start, 20.5%) ;
}
80% ,100%{
top: var(--top-end, 12.8%) ;
transform: translate3d(-20%, 0, 0);
}
}
.product-card__img.back-img {
border-radius: .3em ;
animation: product-card-img-back var(--duration-product-item) forwards ;
}
@keyframes product-card-img-back {
0%, 15%{
top: var(--top-end, 12.8%) ;
transform: translate3d(-20%, 0, 0);
}
50% ,100%{
top: var(--top-start, 20.5%) ;
}
}
.product-card__content {
width: 6em ;
z-index: 50 ;
height: 6.88em ;
cursor: pointer ;
overflow: hidden ;
border-radius: .25em ;
will-change : transform ;
background-color: white ;
text-transform: capitalize ;
box-shadow: -2px 6px 15px 0px #2f2f2f24 ;
transition: opacity calc(var(--duration-product-item) / 2) ;
}
.product-card__content.open-content {
width: 100% ;
height: 100% ;
z-index: 150 ;
cursor: initial ;
box-shadow: none ;
padding-left: 1.05em ;
pointer-events: initial ;
background-color: unset ;
transform: translate3d(0, 180%, 0) ;
animation: open-content var(--duration-product-item) forwards,
opacity-content calc(var(--duration-product-item) * 2) forwards ;
}
@keyframes open-content {
100%{
transform: none ;
}
}
@keyframes opacity-content {
0%{
opacity: 0;
}
100%{
opacity: 1;
}
}
.product-card__info {
padding: .82em .7em ;
}
.product-card__content.open-content .product-card__info {
width: 63% ;
margin-left: auto ;
padding: .8em .7em ;
}
.product-card__product-name {
margin: 0 0 .2em ;
line-height: 1.4 ;
font-size: .63em ;
min-height: 2.778em ;
white-space: pre-line ;
}
.product-card__content.open-content .product-card__product-name {
font-size: .78em ;
line-height: 1.5 ;
margin-bottom: .8em ;
letter-spacing: .03em ;
}
.product-card__product-author {
font-size: .4em ;
font-weight: bold ;
color: #00000073 ;
letter-spacing: -.01em ;
}
.product-card__content.open-content .product-card__product-author {
font-size: .55em ;
}
.product-card__rank {
display: flex ;
align-items: center ;
margin: .8em 0 .75em ;
}
.product-card__content.open-content .product-card__rank {
width: 0 ;
overflow: hidden ;
margin: 1.2em 0 0 ;
animation: show-rank var(--duration-product-item) var(--duration-product-item) forwards ;
}
@keyframes show-rank {
100%{
width: 100% ;
}
}
.product-card__starts {
position: relative ;
}
.product-card__starts::after {
right: 0 ;
content: "" ;
height: 100% ;
width: var(--rank) ;
position: absolute ;
background-color: #ffffffc2 ;
}
.product-card__content.open-content .product-card__starts::after {
background-color: #f8f9fcc9 ;
}
.product-card__start-icon {
width: .5em ;
height: .5em ;
flex-shrink: 0 ;
margin-right: .1em ;
}
.product-card__content.open-content .product-card__start-icon {
animation: show-star var(--duration-product-item) var(--duration-product-item) forwards ;
}
@keyframes show-star {
0% {
transform: scale(0) ;
}
100%{
transform: scale(1) ;
}
}
.product-card__rank-number {
font-size: .5em ;
color: #ffb964 ;
font-weight: bold ;
margin-left: .15em ;
margin-bottom: -.3em ;
letter-spacing: .05em ;
}
.product-card__view {
display: flex ;
font-size: .45em ;
font-weight: bold ;
color: #00000059 ;
position: relative ;
align-items: center ;
}
.product-card__view.hide{
display: none ;
}
.product-card__view::after {
right: 4% ;
opacity: .2 ;
width: 1.6em ;
content: " " ;
height: 1.6em ;
position: absolute ;
transform: rotate(180deg) ;
background-position: center ;
background-repeat: no-repeat ;
background-image: url(http://utopian-drink.surge.sh/images/icon/back.svg) ;
}
.product-card__view-number {
color: #408cff ;
margin-right: .45em ;
}
.product-card__view-value{
font-size: .45em ;
color:#00000059 ;
margin: 0 0 -.3em .8em ;
}
.product-card__view-value.hide{
display: none ;
}
.product-card__buttons {
width: 100% ;
margin-top: 2.8em ;
font-size: 0.45em ;
color: #000000d9 ;
will-change: transform ;
padding: 0 1.6em 2.4em 0 ;
border-bottom: .2em solid #c3c3c31f ;
}
@media (max-width: 26.875em) {
.product-card__buttons {
margin-top: 1.8em ;
padding: 0 1.6em 1.4em 0 ;
}
}
.product-card__button-icon {
width: 1.6em ;
margin-right: .8em ;
}
.product-card__button-text {
margin: .5em 0 0 ;
}
.product-card__context {
height: 100% ;
overflow: hidden ;
font-size: .52em ;
padding: 1.2em 0 0 ;
text-transform: none ;
}
.product-card__context ::first-letter{
text-transform: capitalize ;
}
.product-card__description {
overflow: auto ;
line-height: 1.9 ;
margin: 1.6em 0 0 ;
color: #969696a8 ;
padding-right: 2.15em ;
pointer-events: initial ;
height: calc(100% - 23.2em) ;
}
@media (min-width: 26.875em) {
.product-card__description {
overflow: hidden ;
}
}
.product-card__content.open-content .product-card__description {
transform: translate3d(0, 12%, 0) ;
animation: description var(--duration-product-item) calc(var(--duration-product-item) / 2) forwards;
}
@keyframes description {
100% {
transform: translate3d(0, 0, 0) ;
}
}
.product-card__cta {
opacity: 0 ;
z-index: 300 ;
color: white ;
font-size: .55em ;
margin-left: auto ;
pointer-events: none ;
letter-spacing: 0.1em ;
text-transform: capitalize ;
background-color: #1368fa ;
padding: 1.7em 1.5em 1em 1.9em ;
}
.product-card__item.active .product-card__cta {
opacity: 1 ;
pointer-events: initial ;
transition: opacity calc(var(--duration-product-item) / 2) var(--duration-product-item) ;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment