Skip to content

Commit d2f89af

Browse files
authored
Merge pull request #878 from cornell-dti/countdown
Wrapped Countdown Component
2 parents 99ccd34 + 271af1b commit d2f89af

File tree

8 files changed

+543
-18
lines changed

8 files changed

+543
-18
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"moment": "^2.29.4",
3434
"moment-timezone": "^0.5.32",
3535
"react": "^17.0.2",
36+
"react-confetti-explosion": "^2.1.2",
3637
"react-datepicker": "1.3.0",
3738
"react-dates": "^18.2.2",
3839
"react-dom": "^17.0.2",
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React, { useState, useEffect } from "react";
2+
import "../../styles/WrappedCountdown.scss";
3+
import ConfettiExplosion from "react-confetti-explosion";
4+
import cone from "../../media/wrapped/cone.svg";
5+
import ribbonBall from "../../media/wrapped/ribbonBall.svg";
6+
7+
type WrappedDate = {
8+
launchDate: Date;
9+
startDate: Date;
10+
};
11+
type RemainingTime = {
12+
days: number;
13+
hours: number;
14+
minutes: number;
15+
total: number
16+
};
17+
// Declare interface to takes in the setter setDisplayWrapped as a prop
18+
interface WrappedCountdownProps {
19+
setDisplayWrapped: React.Dispatch<React.SetStateAction<boolean>>; // Define prop type for setter
20+
wrappedDate: WrappedDate;
21+
}
22+
const WrappedCountdown: React.FC<WrappedCountdownProps> = ({ setDisplayWrapped, wrappedDate }) => {
23+
const handleButtonClick = () => {
24+
// Call the setter function to change the state in the parent
25+
setDisplayWrapped(true); // Set to true to show the modal
26+
};
27+
// Helper function to calculate the remaining time in days, hours, and minutes from start date to launch date
28+
const calculateTimeRemaining = (dateProps: WrappedDate): RemainingTime => {
29+
// Calculate the time remaining (in milliseconds) between the launch date and start date
30+
const time = dateProps.launchDate.getTime() - new Date().getTime();
31+
const gap = time < 0 ? 0 : time;
32+
// Calculate days, hours, and minutes remaining
33+
const days = Math.floor(gap / (1000 * 60 * 60 * 24));
34+
const hours = Math.floor((gap / (1000 * 60 * 60)) % 24);
35+
const minutes = Math.floor((gap / (1000 * 60)) % 60);
36+
return { days, hours, minutes, total: gap };
37+
};
38+
// Initialize countdown state using the calculateTimeRemaining function
39+
const [timeRemaining, setTimeRemaining] = useState<RemainingTime>(calculateTimeRemaining(wrappedDate));
40+
const [countDownClicked, setCountDownClicked] = useState<boolean>(false);
41+
const [confettiShown, setConfettiShown] = useState<boolean>(false);
42+
const [isZeroCounter, setIsZeroCounter] = useState<boolean>(false);
43+
44+
// Countdown timer effect
45+
useEffect(() => {
46+
const updatedTime = calculateTimeRemaining(wrappedDate);
47+
setTimeRemaining(updatedTime);
48+
49+
// Stop countdown and set `isZeroCounter` when time reaches zero
50+
if (updatedTime.total <= 0) {
51+
setIsZeroCounter(true);
52+
}
53+
}, [wrappedDate, timeRemaining]);
54+
55+
// Trigger confetti with a delay only the first time `isZeroCounter` becomes true
56+
useEffect(() => {
57+
if (isZeroCounter && !confettiShown) {
58+
setTimeout(() => {
59+
setConfettiShown(true); // Show confetti after a delay
60+
}, 1000); // Delay of 1 second
61+
}
62+
}, [isZeroCounter, confettiShown]);
63+
64+
// Prepend the days, hours, and minutes if they're single digits
65+
const prependZero = (num: number): string => (num < 10 ? `0${num}` : `${num}`);
66+
67+
// Check that today is the start date or dates after the start date, then render the countdown if true
68+
const isStartDate = () => {
69+
const today = new Date();
70+
// To get the Date object with respect to Eastern Time, we must offset
71+
return today.getTime() >= wrappedDate.startDate.getTime();
72+
};
73+
74+
return !isStartDate() ? null : countDownClicked ? (
75+
<div onClick={() => setCountDownClicked(false)}>
76+
<div className="countdownUpdates">
77+
<div className="countdownContainer">
78+
{!isZeroCounter ? (
79+
<>
80+
<div>
81+
<div className="countdownContainer_boxes">{prependZero(timeRemaining.days)}</div>
82+
<p className="counter_sub">DAYS</p>
83+
</div>
84+
<div>
85+
<div className="countdownContainer_boxes">{prependZero(timeRemaining.hours)}</div>
86+
<p className="counter_sub">HOURS</p>
87+
</div>
88+
<div>
89+
<div className="countdownContainer_boxes">{prependZero(timeRemaining.minutes)}</div>
90+
<p className="counter_sub">MINUTES</p>
91+
</div>
92+
<div className="textContainer">
93+
<p className="top">Queue Me In</p>
94+
<p className="bottom">WRAPPED</p>
95+
</div>
96+
</>
97+
) : (
98+
<div className="launch">
99+
<div className="post">
100+
<div className="textContainer">
101+
<p className="top">Queue Me In</p>
102+
<p className="bottom">WRAPPED</p>
103+
</div>
104+
<div className="viewWrap" onClick={handleButtonClick}>
105+
View Now
106+
</div>
107+
</div>
108+
{!confettiShown && <ConfettiExplosion duration={2800} force={0.6} particleCount={200} />}
109+
<img className="cone" src={cone} alt="icon" />
110+
</div>
111+
)}
112+
</div>
113+
</div>
114+
</div>
115+
) : (
116+
<img className="ribbonBall" src={ribbonBall} alt="icon" onClick={() => setCountDownClicked(true)} />
117+
);
118+
};
119+
export default WrappedCountdown;

src/components/pages/SplitView.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ import TopBar from "../includes/TopBar";
1515
import CalendarExportModal from "../includes/CalendarExportModal";
1616
import { RootState } from "../../redux/store";
1717
import { updateCourse, updateSession } from "../../redux/actions/course";
18-
import Browser from '../../media/browser.svg';
19-
import smsNotif from '../../media/smsNotif.svg'
20-
import { addBanner } from '../../redux/actions/announcements';
21-
import Banner from '../includes/Banner';
22-
import FeedbackPrompt from '../includes/FeedbackPrompt';
23-
import Wrapped from '../includes/Wrapped';
18+
import Browser from "../../media/browser.svg";
19+
import smsNotif from "../../media/smsNotif.svg";
20+
import { addBanner } from "../../redux/actions/announcements";
21+
import Banner from "../includes/Banner";
22+
import FeedbackPrompt from "../includes/FeedbackPrompt";
23+
import Wrapped from "../includes/Wrapped";
24+
import WrappedCountdown from "../includes/WrappedCountdown";
25+
import { WRAPPED_START_DATE, WRAPPED_LAUNCH_DATE } from "../../constants";
2426

2527
// Also update in the main LESS file
2628
const MOBILE_BREAKPOINT = 920;
@@ -115,15 +117,15 @@ const SplitView = ({
115117
}, [courseHook, updateCourse]);
116118
useEffect(() => {
117119
updateSession(sessionHook);
118-
}, [sessionHook, updateSession])
120+
}, [sessionHook, updateSession]);
119121

120122
useEffect(() => {
121123
if (user && user.wrapped) {
122124
setDisplayWrapped(true);
123125
} else {
124126
setDisplayWrapped(false);
125127
}
126-
}, [user])
128+
}, [user]);
127129

128130
// Handle browser back button
129131
history.listen((location) => {
@@ -194,6 +196,9 @@ const SplitView = ({
194196
}
195197
}, [addBanner, user]);
196198

199+
const start = new Date(WRAPPED_START_DATE);
200+
const launch = new Date(WRAPPED_LAUNCH_DATE);
201+
197202
return (
198203
<>
199204
<LeaveQueue setShowModal={setShowModal} showModal={showModal} removeQuestion={removeQuestion} />
@@ -258,8 +263,8 @@ const SplitView = ({
258263
<div className="warningArea">
259264
<div>&#9888;</div>
260265
<div>
261-
Please make sure to enable browser notifications in your system
262-
settings.
266+
Please make sure to enable browser notifications in your system
267+
settings.
263268
</div>
264269
</div>
265270
)}
@@ -270,16 +275,18 @@ const SplitView = ({
270275
<Loader active={true} content="Loading" />
271276
))}
272277
<ProductUpdates />
278+
<WrappedCountdown
279+
setDisplayWrapped={setDisplayWrapped}
280+
wrappedDate={{ launchDate: launch, startDate: start }}
281+
/>
273282
{displayFeedbackPrompt ? (
274283
<FeedbackPrompt
275284
onClose={submitFeedback(removedQuestionId, course, session.sessionId)}
276285
closeFeedbackPrompt={() => setDisplayFeedbackPrompt(false)}
277286
/>
278287
) : null}
279288

280-
{displayWrapped ? (
281-
<Wrapped user={user} onClose={() => setDisplayWrapped(false)} />
282-
) : null}
289+
{displayWrapped ? <Wrapped user={user} onClose={() => setDisplayWrapped(false)} /> : null}
283290
</>
284291
);
285292
};

src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ export const ALL_SEMESTERS = ['SP20', 'FA20', 'SP21', 'FA21', 'SP22', 'FA22',
44

55
export const START_DATE = '2024-08-19'
66
export const END_DATE = '2024-12-20'
7+
8+
// These are the start date and launch date for QMI Wrap for the current semester
9+
export const WRAPPED_START_DATE = "2024-11-12T00:00:00";
10+
export const WRAPPED_LAUNCH_DATE = "2024-11-22T00:00:00";

src/media/wrapped/cone.svg

Lines changed: 22 additions & 0 deletions
Loading

src/media/wrapped/ribbonBall.svg

Lines changed: 19 additions & 0 deletions
Loading

src/styles/WrappedCountdown.scss

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
.countdownUpdates {
2+
position: fixed;
3+
bottom: 35px;
4+
right: 41px;
5+
}
6+
.countdownContainer {
7+
padding: 20px;
8+
display: flex;
9+
flex-direction: row;
10+
justify-content: space-between;
11+
align-items: center;
12+
width: 413px;
13+
height: 139px;
14+
border-radius: 12px;
15+
background: linear-gradient(63deg, #668ae9 16.65%, #6db9ea 83.35%); /* Gradient background */
16+
}
17+
.countdownContainer_boxes {
18+
color: #5599db;
19+
font-family: Roboto;
20+
font-size: 50px;
21+
font-style: normal;
22+
font-weight: 400;
23+
line-height: normal;
24+
justify-content: center;
25+
align-items: center;
26+
text-align: center;
27+
display: flex;
28+
width: 63px;
29+
height: 76px;
30+
border-radius: 5px;
31+
background: #fff;
32+
}
33+
.textContainer {
34+
align-items: center;
35+
}
36+
37+
.ribbonBall {
38+
position: fixed;
39+
bottom: 36px;
40+
right: 46.23px;
41+
width: 68px;
42+
height: 68px;
43+
border-width: 100px;
44+
cursor: pointer;
45+
animation: rotation 1.3s ease-in-out infinite;
46+
}
47+
48+
@keyframes rotation {
49+
50+
0% {
51+
transform: rotate(15deg);
52+
}
53+
15% {
54+
transform: rotate(-15deg);
55+
}
56+
25% {
57+
transform: rotate(0deg);
58+
}
59+
}
60+
61+
.top {
62+
margin: 0;
63+
color: #fff;
64+
font-family: Roboto;
65+
font-size: 24.079px;
66+
font-style: normal;
67+
font-weight: 700;
68+
line-height: normal;
69+
}
70+
.bottom {
71+
margin: 0;
72+
font-family: Roboto;
73+
font-size: 24.079px;
74+
font-style: normal;
75+
font-weight: 400;
76+
line-height: normal;
77+
background: linear-gradient(180deg, #fff 0%, #fff 100%);
78+
background-clip: text;
79+
-webkit-background-clip: text;
80+
-webkit-text-fill-color: transparent;
81+
}
82+
.counter_sub {
83+
margin-top: 10px;
84+
color: #fff;
85+
font-family: Roboto;
86+
font-size: 12px;
87+
font-style: normal;
88+
font-weight: 500;
89+
line-height: normal;
90+
letter-spacing: -0.6px;
91+
}
92+
.viewWrap {
93+
cursor: pointer;
94+
margin-top: 7px;
95+
width: 154.619px;
96+
height: 37.669px;
97+
border-radius: 5px;
98+
background: #fff;
99+
color: #5599db;
100+
font-family: Roboto;
101+
font-size: 20px;
102+
font-style: normal;
103+
font-weight: 500;
104+
line-height: normal;
105+
display: flex;
106+
justify-content: center;
107+
align-items: center;
108+
text-align: center;
109+
}
110+
111+
.post {
112+
display: flex;
113+
flex-direction: column;
114+
}
115+
116+
.cone {
117+
z-index:10;
118+
margin-top: 40px;
119+
margin-right: 25px;
120+
}
121+
122+
.launch{
123+
margin-left: 20px;
124+
display: flex;
125+
flex-direction: row;
126+
justify-content: center;
127+
align-items: center;
128+
}

0 commit comments

Comments
 (0)