1// This template requires the 'motion' animation library to be installed:
2//
3// npm install motion
4
5"use client";
6
7import { useAnimate } from "motion/react";
8import { useEffect, useRef, useState } from "react";
9import { Badge } from "../ui/badge";
10
11// NOTE: Change this date to whatever date you want to countdown to :)
12let COUNTDOWN_FROM = "2024-12-31";
13// Remove the below 4 lines
14const now = new Date();
15COUNTDOWN_FROM = new Date(COUNTDOWN_FROM) < now
16 ? `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate() + 2}`
17 : COUNTDOWN_FROM;
18
19const SECOND = 1000;
20const MINUTE = SECOND * 60;
21const HOUR = MINUTE * 60;
22const DAY = HOUR * 24;
23
24type Units = "Day" | "Hour" | "Minute" | "Second";
25
26const Countdown1 = () => {
27 return (
28 <div>
29 <div className="container flex flex-col items-center text-center">
30 <Badge variant="outline" >Exciting announcement 🎉</Badge>
31 <h2 className="mb-6 mt-2 text-pretty text-2xl font-bold lg:text-3xl">
32 We're going live on Product Hunt in
33 </h2>
34 </div>
35 <div className="mx-auto flex w-full max-w-5xl items-center bg-white">
36 <CountdownItem unit="Day" text="days" />
37 <CountdownItem unit="Hour" text="hours" />
38 <CountdownItem unit="Minute" text="minutes" />
39 <CountdownItem unit="Second" text="seconds" />
40 </div>
41 </div>
42 );
43};
44
45const CountdownItem = ({ unit, text }: { unit: Units; text: string }) => {
46 const { ref, time } = useTimer(unit);
47
48 return (
49 <div className="flex h-24 w-1/4 flex-col items-center justify-center gap-1 border-r-[1px] border-slate-200 font-mono md:h-36 md:gap-2">
50 <div className="relative w-full overflow-hidden text-center">
51 <span
52 ref={ref}
53 className="block text-2xl font-medium text-black md:text-4xl lg:text-6xl xl:text-7xl"
54 >
55 {time}
56 </span>
57 </div>
58 <span className="text-xs font-light text-slate-500 md:text-sm lg:text-base">
59 {text}
60 </span>
61 </div>
62 );
63};
64
65export default Countdown1;
66
67// NOTE: Framer motion exit animations can be a bit buggy when repeating
68// keys and tabbing between windows. Instead of using them, we've opted here
69// to build our own custom hook for handling the entrance and exit animations
70const useTimer = (unit: Units) => {
71 const [ref, animate] = useAnimate();
72
73 const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
74 const timeRef = useRef(0);
75
76 const [time, setTime] = useState(0);
77
78 useEffect(() => {
79 const targetDate = new Date(COUNTDOWN_FROM);
80 intervalRef.current = setInterval(handleCountdown.bind(null, targetDate), 1000);
81
82 return () => clearInterval(intervalRef.current || undefined);
83 }, []);
84
85 const handleCountdown = async (targetDate: Date) => {
86 const now = new Date();
87 const end = targetDate
88 const distance = +end - +now;
89
90 let newTime = 0;
91
92 if (unit === "Day") {
93 newTime = Math.floor(distance / DAY);
94 } else if (unit === "Hour") {
95 newTime = Math.floor((distance % DAY) / HOUR);
96 } else if (unit === "Minute") {
97 newTime = Math.floor((distance % HOUR) / MINUTE);
98 } else {
99 newTime = Math.floor((distance % MINUTE) / SECOND);
100 }
101
102 if (newTime !== timeRef.current) {
103 // Exit animation
104 await animate(
105 ref.current,
106 { y: ["0%", "-50%"], opacity: [1, 0] },
107 { duration: 0.35 }
108 );
109
110 timeRef.current = newTime;
111 setTime(newTime);
112
113 // Enter animation
114 await animate(
115 ref.current,
116 { y: ["50%", "0%"], opacity: [0, 1] },
117 { duration: 0.35 }
118 );
119 }
120 };
121
122 return { ref, time };
123};