Skip to content

Instantly share code, notes, and snippets.

@xtrasmal
Last active March 20, 2019 08:58
Show Gist options
  • Save xtrasmal/9f56aa278237c2e507cd145071a9c9d5 to your computer and use it in GitHub Desktop.
Save xtrasmal/9f56aa278237c2e507cd145071a9c9d5 to your computer and use it in GitHub Desktop.
VueJS Flip countdown flip clock using momentjs
<script>
import * as moment from 'moment-timezone'
export default {
props: {
date: {
required: true,
default: moment().format()
},
locale: {
required: false,
default: 'en'
}
},
data: () => ({
now: null,
interval: null,
running: true
}),
mounted() {
this.now = moment();
if ( window['requestAnimationFrame'] ) {
this.update()
} else {
this.interval = setInterval(() => {
this.update()
}, 1000);
}
},
beforeDestroy () {
this.clear();
},
methods: {
update() {
this.now = moment();
if ( window['requestAnimationFrame'] ) {
this.interval = requestAnimationFrame(this.update.bind(this));
}
},
clear() {
this.running = false
if ( window['cancelAnimationFrame'] ) {
cancelAnimationFrame(this.interval);
} else {
clearInterval(this.interval)
}
},
toArray(number) {
let output = [];
if(typeof number !== 'string') {
number = number.toString();
}
for (var i = 0, len = number.length; i < len; i += 1) {
output.push(+number.charAt(i));
}
return output
},
twoDigits(number) {
return ( number < 10 && number > -1 ? '0' : '' ) + number;
}
},
computed: {
timeLeft() {
let seconds = moment(this.date).locale(this.locale).diff(this.now, 'seconds')
if(seconds <= 0) {
this.clear()
return 0
}
return seconds
},
time() {
return {
running: this.running,
seconds: this.timeLeft % 60 || 0,
minutes: Math.trunc(this.timeLeft / 60) % 60 || 0,
hours: Math.trunc(this.timeLeft / 60 / 60) % 24 || 0,
days: Math.trunc(this.timeLeft / 60 / 60 / 24 || 0)
}
}
},
render() {
return this.$scopedSlots.default({
toArray: number => this.toArray(number),
twoDigits: number => this.twoDigits(number),
timePart: part => this.time[part],
time: this.time
})
}
}
</script>
.flip-clock {
text-align: center;
-webkit-perspective: 600px;
perspective: 600px;
margin: 0 auto;
}
.flip-clock *,
.flip-clock *:before,
.flip-clock *:after {
box-sizing: border-box;
}
.flip-clock__piece {
display: inline-block;
margin: 0 0.2vw;
}
@media (min-width: 1000px) {
.flip-clock__piece {
margin: 0 5px;
}
}
.flip-clock__slot {
font-size: 1rem;
line-height: 1.5;
display: block;
}
.flip-card {
display: block;
position: relative;
padding-bottom: 0.72em;
font-size: 2.25rem;
line-height: 0.95;
}
@media (min-width: 1000px) {
.flip-clock__slot {
font-size: 1.2rem;
}
.flip-card {
font-size: 3rem;
}
}
.flip-card__top,
.flip-card__bottom,
.flip-card__back-bottom,
.flip-card__back::before,
.flip-card__back::after {
display: block;
height: 0.72em;
color: #ccc;
background: #222;
padding: 0.23em 0.25em 0.4em;
border-radius: 0.15em 0.15em 0 0;
backface-visibility: hidden;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
width: 1.8em;
}
.flip-card__bottom,
.flip-card__back-bottom {
color: #FFF;
position: absolute;
top: 50%;
left: 0;
border-top: solid 1px #000;
background: #393939;
border-radius: 0 0 0.15em 0.15em;
pointer-events: none;
overflow: hidden;
z-index: 2;
}
.flip-card__back-bottom {
z-index: 1;
}
.flip-card__bottom::after,
.flip-card__back-bottom::after {
display: block;
margin-top: -0.72em;
}
.flip-card__back::before,
.flip-card__bottom::after,
.flip-card__back-bottom::after {
content: attr(data-value);
}
.flip-card__back {
position: absolute;
top: 0;
height: 100%;
left: 0%;
pointer-events: none;
}
.flip-card__back::before {
position: relative;
overflow: hidden;
z-index: -1;
}
.flip .flip-card__back::before {
z-index: 1;
-webkit-animation: flipTop 0.3s cubic-bezier(0.37, 0.01, 0.94, 0.35);
animation: flipTop 0.3s cubic-bezier(0.37, 0.01, 0.94, 0.35);
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-transform-origin: center bottom;
transform-origin: center bottom;
}
.flip .flip-card__bottom {
-webkit-transform-origin: center top;
transform-origin: center top;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation: flipBottom 0.6s cubic-bezier(0.15, 0.45, 0.28, 1);
animation: flipBottom 0.6s cubic-bezier(0.15, 0.45, 0.28, 1);
}
@-webkit-keyframes flipTop {
0% {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
z-index: 2;
}
0%,
99% {
opacity: 1;
}
100% {
-webkit-transform: rotateX(-90deg);
transform: rotateX(-90deg);
opacity: 0;
}
}
@keyframes flipTop {
0% {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
z-index: 2;
}
0%,
99% {
opacity: 1;
}
100% {
-webkit-transform: rotateX(-90deg);
transform: rotateX(-90deg);
opacity: 0;
}
}
@-webkit-keyframes flipBottom {
0%,
50% {
z-index: -1;
-webkit-transform: rotateX(90deg);
transform: rotateX(90deg);
opacity: 0;
}
51% {
opacity: 1;
}
100% {
opacity: 1;
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
z-index: 5;
}
}
@keyframes flipBottom {
0%,
50% {
z-index: -1;
-webkit-transform: rotateX(90deg);
transform: rotateX(90deg);
opacity: 0;
}
51% {
opacity: 1;
}
100% {
opacity: 1;
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
z-index: 5;
}
}
<template>
<countdown class="flip-clock" :date="date" :locale="locale" @completed="completed">
<div slot-scope="{time}">
<FlipCountdownPart v-for="(part, index) in parts" :key="index" :part="part" :time="time" ref="parts"></FlipCountdownPart>
</div>
</countdown>
</template>
<script>
import Countdown from '~/components/utils/countdown/Countdown'
import FlipCountdownPart from '~/components/utils/countdown/FlipCountdownPart'
export default {
props: {
parts: {
required: true
},
date: {
required: true,
},
locale: {
required: false,
default: 'en'
}
},
methods: {
completed(status) {
this.$emit('completed', status)
}
},
components: {
Countdown, FlipCountdownPart
}
}
</script>
<template>
<span v-show="show" ref="flipclock" class="flip-clock__piece">
<span class="flip-clock__card flip-card">
<b class="flip-card__top">{{twoDigits(current)}}</b>
<b class="flip-card__bottom" :data-value="twoDigits(current)"></b>
<b class="flip-card__back" :data-value="twoDigits(previous)"></b>
<b class="flip-card__back-bottom" :data-value="twoDigits(previous)"></b>
</span>
<span class="flip-clock__slot">{{part}}</span>
</span>
</template>
<script>
export default {
props: ['time', 'part'],
data: () => ({
current: 0,
previous: 0,
show: true
}),
methods: {
twoDigits(value) {
return ( value < 10 && value > -1 ? '0' : '' ) + value;
}
},
watch: {
time(newValue) {
if ( newValue[this.part] === undefined ) {
this.show = false;
return;
}
let val = newValue[this.part];
this.show = true;
val = ( val < 0 ? 0 : val );
if ( val !== this.current ) {
this.previous = this.current;
this.current = val;
this.$refs.flipclock.classList.remove('flip');
void this.$refs.flipclock.offsetWidth;
this.$refs.flipclock.classList.add('flip');
}
}
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment