Elements
Wheel of fortune
20 Dec 2023
Готовий компонент для реалізації колеса фортуни на проекті
HTML
PostCSS
JS
<button id="openBtn" class="w-fit rounded bg-white p-2 text-red-500">
open wheel
</button>
<div id="wheel-popup" class="wheel">
<div class="wheel__wrap">
<div class="wheel__content-wrap">
<button
class="wheel__close-btn"
title="close"
aria-label="close popup"
></button>
<div class="wheel__info-wrap">
<div class="wheel__info-title">
Black
<span class="wheel__info-title-accent">friday!</span>
</div>
<div class="wheel__info-description content-element">
<p>We have prepared a gift for you.</p>
<p>Spin the wheel and win a gift from us.</p>
</div>
<div class="wheel__input-wrap">
<div class="wheel-input" id="black-friday-phone-number">
<input
name="tel"
class="phone-input"
type="tel"
value=""
placeholder="Phone"
/>
<span class="input-info"></span>
</div>
<button class="wheel-start">Spin the wheel!</button>
</div>
<div class="wheel__info-additional">
<div class="wheel__info-end-wrap">
<div class="wheel__info-end-title">
UNTIL THE END OF THE PROMOTION
</div>
<div
class="wheel__info-end-timer"
data-end-date="1705658460000"
>
<div class="wheel__info-timer-item">
<div class="wheel__info-timer-item-num" id="days">
00
</div>
<div class="wheel__info-timer-item-text">Days</div>
</div>
<div class="wheel__info-timer-item">
<div class="wheel__info-timer-item-num" id="hours">
00
</div>
<div class="wheel__info-timer-item-text">Hours</div>
</div>
<div class="wheel__info-timer-item">
<div class="wheel__info-timer-item-num" id="minutes">
00
</div>
<div class="wheel__info-timer-item-text">Minutes</div>
</div>
<div class="wheel__info-timer-item">
<div class="wheel__info-timer-item-num" id="seconds">
00
</div>
<div class="wheel__info-timer-item-text">Seconds</div>
</div>
</div>
</div>
<div class="wheel__info-prize">
<div class="wheel__info-end-title">Your gift</div>
<div class="wheel__info-prize-text">
<span> </span>
<span class="wheel__info-prize-text-accent"> </span>
</div>
</div>
</div>
</div>
<div class="wheel__wheel">
<img
src="./assets/images/wheel-border-bg.svg"
class="wheel__wheel-border-bg"
aria-hidden
alt="bg"
/>
<div class="wheel__wheel-list-wrap">
<ul class="wheel__wheel-item-list">
<li class="wheel__wheel-item" data-id="8">
<div class="wheel__wheel-item-title">Test 1</div>
<div class="wheel__wheel-item-icon">
<img
src="./assets/images/gift-blue.png"
alt="bag icon"
/>
</div>
</li>
<li class="wheel__wheel-item" data-id="7">
<div class="wheel__wheel-item-title">Test 2</div>
<div class="wheel__wheel-item-icon">
<img
src="./assets/images/gift-blue.png"
alt="bag icon"
/>
</div>
</li>
<li class="wheel__wheel-item" data-id="6">
<div class="wheel__wheel-item-title">Test 3</div>
<div class="wheel__wheel-item-icon">
<img
src="./assets/images/gift-blue.png"
alt="bag icon"
/>
</div>
</li>
<li class="wheel__wheel-item" data-id="5">
<div class="wheel__wheel-item-title">Test 4</div>
<div class="wheel__wheel-item-icon">
<img
src="./assets/images/gift-blue.png"
alt="bag icon"
/>
</div>
</li>
<li class="wheel__wheel-item" data-id="4">
<div class="wheel__wheel-item-title">Test 5</div>
<div class="wheel__wheel-item-icon">
<img
src="./assets/images/gift-blue.png"
alt="bag icon"
/>
</div>
</li>
<li class="wheel__wheel-item" data-id="3">
<div class="wheel__wheel-item-title">Test 6</div>
<div class="wheel__wheel-item-icon">
<img
src="./assets/images/gift-blue.png"
alt="bag icon"
/>
</div>
</li>
<li class="wheel__wheel-item" data-id="2">
<div class="wheel__wheel-item-title">Discount 25%</div>
<div class="wheel__wheel-item-icon">
<img
src="./assets/images/gift-blue.png"
alt="bag icon"
/>
</div>
</li>
<li class="wheel__wheel-item" data-id="1">
<div class="wheel__wheel-item-title">Discount 20%</div>
<div class="wheel__wheel-item-icon">
<img
src="./assets/images/gift-blue.png"
alt="bag icon"
/>
</div>
</li>
</ul>
<button class="wheel__wheel-button">
<img src="./assets/images/gift.svg" alt="gift icon" />
<span class="wheel__wheel-button-text">Start</span>
<div class="wheel__wheel-prize-box-wrap-animation">
<div class="wheel__wheel-prize-box-wrap">
<div class="wheel__wheel-prize-text">
<span> </span>
<span class="wheel__wheel-prize-text-accent">
</span>
</div>
<img
src="./assets/images/box-top.svg"
alt=""
class="wheel__wheel-box-top"
/>
<img
src="./assets/images/box-bottom.svg"
alt=""
class="wheel__wheel-box-bottom"
/>
</div>
</div>
</button>
<div class="wheel__wheel-arrow">
<img
src="./assets/images/wheel-arrow.svg"
aria-hidden
alt="arrow"
class="wheel__wheel-arrow-image"
/>
</div>
<div class="wheel__wheel-prize-wrap">
<img
src="./assets/images/black-friday-gift-bg.png"
class="wheel__wheel-prize-bg"
aria-hidden=""
alt="gift"
/>
<div id="confetti"></div>
</div>
</div>
</div>
</div>
</div>
</div>
.wheel {
--white: #ffffff;
--blue: #074b60;
--dark-blue: #002f42;
--cl-primary: #e63041;
--moonstone: #80a5b0;
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(27, 4, 1, 0.6);
opacity: 0;
visibility: hidden;
transition: all ease 500ms;
pointer-events: none;
&.is-show {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
}
.wheel__wrap {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
}
.wheel__content-wrap {
position: relative;
display: flex;
align-items: center;
width: 95%;
max-width: 1280px;
margin: 0 auto;
@mixin media 951 {
flex-direction: column-reverse;
}
}
.wheel__close-btn {
position: absolute;
top: 0;
right: 0;
z-index: 6;
width: 40px;
height: 40px;
border: 2px solid var(--cl-primary);
border-radius: 999px;
background: var(--white);
transition: background ease 250ms;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 2px;
background: var(--cl-primary);
transition: background ease 250ms;
}
&::before {
transform: translate(-50%, -50%) rotate(-45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(45deg);
}
&:hover {
background: var(--cl-primary);
&::before,
&::after {
background: var(--white);
}
}
}
.wheel__info-wrap {
--wrap-pading: ac(48px, 16px);
position: relative;
width: calc(50% - 72px);
flex-shrink: 0;
padding: var(--wrap-pading);
color: var(--white);
transition: all ease 250ms;
@mixin media 951 {
width: 100%;
max-width: ac(450px, 312px, 451, 951);
}
@media (max-height: 700px) and (max-width: 376px) {
padding-bottom: 0;
}
@mixin media 365 {
max-width: 285px;
}
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
z-index: -1;
width: 150%;
height: 100%;
border-radius: 24px;
background: var(--blue);
@mixin media 951 {
top: unset;
bottom: 0;
transform: unset;
width: 100%;
height: 125%;
border-radius: 40px;
}
}
}
.wheel__info-title {
margin-bottom: 12px;
font-size: ac(40px, 22px);
font-weight: 800;
text-transform: uppercase;
line-height: 1;
transition: opacity ease 250ms;
@mixin media 951 {
text-align: center;
}
.wheel__info-title-accent {
display: inline-block;
padding: ac(14px, 8px) ac(8px, 6px);
background: var(--cl-primary);
}
}
.wheel__info-description {
margin-bottom: ac(32px, 16px);
transition: opacity ease 250ms;
@mixin media 951 {
text-align: center;
}
* {
color: var(--white);
font-size: 18px;
font-weight: 400;
line-height: 1.3333;
}
}
.wheel__info-additional {
position: relative;
padding: ac(24px, 16px) 32px ac(32px, 16px);
margin-bottom: ac(36px, 16px);
@mixin media 951 {
width: calc(100% + var(--wrap-pading) * 2);
left: calc(var(--wrap-pading) * -1);
overflow: hidden;
}
@media (max-height: 700px) and (max-width: 376px) {
padding: 10px 20px;
margin-bottom: 0;
border-radius: 0 0 40px 40px;
}
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
z-index: -1;
width: 150%;
height: 100%;
border: 1px solid var(--dark-blue);
border-radius: 16px;
background: #064356;
@mixin media 951 {
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
border-left: none;
border-right: none;
border-radius: 0;
}
@media (max-height: 700px) and (max-width: 376px) {
border-radius: 0 0 40px 40px;
}
}
}
.wheel__info-end-wrap {
position: relative;
}
.wheel__info-prize {
display: none;
}
.wheel__info-end-title {
position: relative;
width: fit-content;
margin-bottom: 16px;
color: var(--cl-primary);
font-size: ac(24px, 14px);
font-weight: 800;
text-transform: uppercase;
@mixin media 951 {
margin: 0 auto 14px;
/* width: 100%;
text-align: center; */
}
&::after {
content: '';
position: absolute;
left: calc(100% + 20px);
top: 50%;
transform: translateY(-50%);
height: 1px;
width: 200%;
background: var(--moonstone);
}
&::before {
display: none;
content: '';
position: absolute;
right: calc(100% + 20px);
top: 50%;
transform: translateY(-50%);
height: 1px;
width: 200%;
background: var(--moonstone);
@mixin media 951 {
display: block;
}
}
}
.wheel__info-end-timer {
display: flex;
align-items: center;
@mixin media 951 {
justify-content: center;
}
}
.wheel__info-timer-item {
padding: ac(15px, 3px) ac(20px, 13px);
border-left: 1px solid var(--moonstone);
border-right: 1px solid var(--moonstone);
&:first-child {
padding-right: ac(28px, 13px);
border-left: none;
}
&:last-child {
padding-left: ac(28px, 13px);
border-right: none;
}
}
.wheel__info-timer-item-num {
margin-bottom: ac(20px, 12px);
font-size: ac(40px, 22px);
font-weight: 800;
line-height: 1;
text-align: center;
@media (max-height: 700px) and (max-width: 376px) {
margin-bottom: 8px;
}
}
.wheel__info-timer-item-text {
color: var(--moonstone);
font-size: ac(14px, 12px);
font-weight: 800;
text-align: center;
text-transform: uppercase;
}
.wheel-start {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
max-height: 40px;
@mixin media 951 {
width: 100%;
}
}
.wheel__wheel {
position: relative;
z-index: 1;
/* overflow: hidden; */
display: flex;
flex-shrink: 0;
width: ac(724px, 450px);
height: ac(724px, 450px);
/* aspect-ratio: 1/1; */
padding: ac(32px, 8px);
border-radius: 9999px;
border: 1px solid var(--moonstone);
background: var(--blue);
transition: all ease 250ms;
@mixin media 951 {
width: 200px;
height: 200px;
}
@media (max-height: 700px) and (max-width: 376px) {
width: 160px;
height: 160px;
}
@mixin media 651 {
/* width: ac(342px, 300px, 360, 375);
height: ac(342px, 300px, 360, 375); */
}
}
.wheel__wheel-border-bg {
position: absolute;
inset: 0;
mix-blend-mode: color-dodge;
border-radius: 99999px;
}
.wheel__wheel-list-wrap {
position: relative;
width: 100%;
height: 100%;
padding: ac(18px, 8px);
border-radius: 9999px;
background: var(--dark-blue);
}
.wheel__wheel-item-list {
position: relative;
width: 100%;
height: 100%;
border-radius: 9999px;
box-shadow: 0px 0px 32px 0px rgba(255, 255, 255, 0.16) inset,
0px 0px 6px 0px rgba(255, 255, 255, 0.32) inset;
/* &.animate {
transition: transform ease-out 7s;
} */
/*
animation: infiniteRotate 60s linear infinite;
@keyframes infiniteRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} */
}
.wheel__wheel-item {
position: absolute;
left: 50%;
top: 50%;
display: flex;
justify-content: end;
align-items: center;
gap: 8px;
width: 50%;
font-size: 16px;
font-weight: 800;
text-transform: uppercase;
padding-left: ac(80px, 30px);
padding-right: ac(20px, 10px);
transform-origin: left center;
transform: translateY(-50%);
@mixin media 951 {
gap: 4px;
padding-left: 30px;
padding-right: 5px;
}
&:nth-child(even) {
color: var(--white);
.wheel__wheel-item-icon {
img {
filter: brightness(0) invert(100%);
}
}
}
}
.wheel__wheel-item-title {
font-size: 12px;
@mixin max-line-length 1;
@mixin media 951 {
opacity: 0;
}
@mixin media 651 {
font-size: 9px;
}
}
.wheel__wheel-item-icon {
flex-shrink: 0;
width: ac(64px, 26px);
height: ac(64px, 26px);
img {
width: 100%;
height: 100%;
object-fit: contain;
}
@mixin media 951 {
opacity: 0;
}
}
.wheel__wheel-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
width: ac(140px, 64px, 551);
height: ac(140px, 64px, 551);
border: ac(7px, 3px) solid var(--light-blue);
border-radius: 999px;
background: var(--blue);
color: var(--white);
filter: drop-shadow(0px 0px 40px rgba(0, 0, 0, 0.16))
drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.16));
transition: all ease 250ms;
&:disabled {
cursor: not-allowed;
.wheel__wheel-button-text {
opacity: 0.5;
}
& > img {
opacity: 0.5;
}
}
&:not(&:disabled):hover {
background: var(--dark-blue);
}
&:not(&:disabled):active {
transform: translate(-50%, -50%) scale(0.9);
}
@mixin media 951 {
width: 64px;
height: 64px;
& > img {
display: none;
}
}
@mixin media 365 {
width: 48px;
height: 48px;
img {
display: none;
}
}
& > img {
width: ac(48px, 24px);
height: ac(48px, 24px);
transition: opacity ease 250ms;
}
/* &::before {
display: none;
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 30px;
background: var(--cl-primary);
border-radius: 999px;
animation: pusle 1000ms linear infinite;
@keyframes pusle {
0% {
transform: translate(-50%, -50%) scale(0.5);
opacity: 0;
}
50% {
transform: translate(-50%, -50%) scale(1.5);
opacity: 0.75;
}
100% {
transform: translate(-50%, -50%) scale(2);
opacity: 0;
}
}
} */
}
.wheel__wheel-button-text {
font-size: ac(16px, 10px);
font-weight: 800;
text-transform: uppercase;
transition: opacity ease 250ms;
}
.wheel__wheel-arrow {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
width: ac(47px, 24px);
}
.wheel__wheel-prize-wrap {
position: absolute;
inset: 0;
overflow: hidden;
/* width: 100%;
height: 100%; */
border: 18px solid var(--dark-blue);
border-radius: 9999px;
background: var(--blue);
opacity: 0;
visibility: hidden;
transition: all ease 250ms;
&::before {
content: '';
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(
0deg,
rgba(7, 75, 96, 0.8) 0%,
rgba(7, 75, 96, 0.8) 100%
);
}
&::after {
content: '';
position: absolute;
inset: 0;
z-index: 0;
background: linear-gradient(0deg, #074b60 0%, #074b60 100%);
background-blend-mode: color;
}
}
.wheel__wheel-prize-bg {
position: relative;
z-index: 1;
mix-blend-mode: luminosity;
width: 100%;
height: 100%;
object-fit: cover;
}
.wheel__wheel-prize-box-wrap-animation {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 3;
opacity: 0;
}
.wheel__wheel-prize-box-wrap {
position: relative;
top: -5px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all cubic-bezier(0.28, -0.1, 0.78, 3) 500ms;
width: 400%;
height: 400%;
transform: translateX(-50%) scale(0.25);
left: 50%;
filter: none;
-webkit-filter: blur(0px);
-moz-filter: blur(0px);
-ms-filter: blur(0px);
filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius='0');
backface-visibility: hidden;
}
.wheel__wheel-prize-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -25%) scale(0);
z-index: -1;
width: 200%;
opacity: 0;
transition: all ease 500ms 550ms;
span {
display: block;
width: fit-content;
padding: 0 ac(13px, 10px);
margin: 0 auto;
color: var(--blue);
background: var(--white);
font-size: ac(32px, 16px);
font-weight: 800;
line-height: 1.4;
text-transform: uppercase;
transition: transform ease 500ms 250ms;
&:not(:last-child) {
margin-bottom: 6px;
}
&.accent {
padding: ac(5px, 0px) ac(5px, 2px);
background: var(--cl-primary);
color: var(--white);
}
transform: rotate(-6.6deg);
}
}
#confetti {
position: absolute;
inset: 0;
z-index: 4;
pointer-events: none;
}
.wheel__wheel-box-top {
width: ac(219px, 140px);
object-position: bottom center;
object-fit: contain;
}
.wheel__wheel-box-bottom {
width: ac(219px, 140px);
object-position: top center;
object-fit: contain;
}
.wheel__wheel-box-top,
.wheel__wheel-box-bottom {
transition: all ease 500ms 500ms;
filter: none;
-webkit-filter: blur(0px);
-moz-filter: blur(0px);
-ms-filter: blur(0px);
filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius='0');
backface-visibility: hidden;
}
.wheel__info-prize {
.wheel__info-end-title {
color: var(--moonstone);
}
}
.wheel__info-prize-text {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
@mixin media 951 {
justify-content: center;
}
span {
font-size: ac(24px, 18px);
font-weight: 800;
text-transform: uppercase;
&.accent {
padding: 0 ac(12px, 5px);
background: var(--cl-primary);
font-size: ac(40px, 18px);
}
}
}
.wheel__input-wrap {
display: flex;
align-items: start;
gap: 10px;
/* flex-wrap: wrap; */
margin-bottom: 20px;
@mixin media 951 {
flex-direction: column;
}
@media (max-height: 700px) and (max-width: 376px) {
margin-bottom: 12px;
}
.default-input {
width: 100%;
}
}
/* Prize animation */
.wheel.wheel-rotate-finished {
.wheel__info-wrap {
@mixin media 951 {
margin-top: -150px;
}
}
.wheel__info-description,
.wheel__info-title {
@mixin media 951 {
opacity: 0;
}
}
.wheel__wheel {
@mixin media 951 {
width: ac(724px, 450px);
height: ac(724px, 450px);
}
@mixin media 651 {
width: ac(342px, 300px, 360, 375);
height: ac(342px, 300px, 360, 375);
}
}
.wheel__wheel-item {
padding-left: 30px;
padding-right: 15px;
}
.wheel__wheel-item-icon,
.wheel__wheel-item-title {
@mixin media 951 {
opacity: 1;
}
}
.wheel__wheel-button {
background: transparent;
border-color: transparent;
.wheel__wheel-button-text {
opacity: 0;
}
& > img {
opacity: 0;
}
&::before {
display: block;
}
@mixin media 951 {
width: ac(96px, 64px, 551, 961);
height: ac(96px, 64px, 551, 961);
}
}
.wheel__wheel-prize-wrap {
opacity: 1;
visibility: visible;
}
.wheel__wheel-prize-box-wrap-animation {
opacity: 1;
}
.wheel__wheel-box-top {
opacity: 1;
transform: translate(-10%, -60%) rotate(-16deg);
@mixin media 551 {
transform: translate(-10%, -80%) rotate(-16deg);
}
}
.wheel__wheel-box-bottom {
opacity: 1;
transform: translateY(70%);
@mixin media 551 {
transform: translateY(85%);
}
}
.wheel__wheel-prize-box-wrap {
opacity: 1;
/* transform: scale(4); */
transform: translateX(-50%) scale(1);
}
.wheel__wheel-prize-text {
opacity: 1;
transform: translate(-50%, -50%);
}
.wheel__info-prize {
display: block;
}
.wheel__info-end-wrap {
display: none;
}
}
.wheel.animating {
.wheel__info-wrap {
@mixin media 951 {
margin-top: -150px;
}
}
.wheel__info-description,
.wheel__info-title {
@mixin media 951 {
opacity: 0;
}
}
.wheel__wheel-button {
.wheel__wheel-button-text {
opacity: 0;
}
& > img {
opacity: 0;
}
&::before {
display: block;
}
@mixin media 951 {
width: ac(96px, 64px, 551, 961);
height: ac(96px, 64px, 551, 961);
}
}
.wheel__wheel {
@mixin media 951 {
width: ac(724px, 450px);
height: ac(724px, 450px);
}
@mixin media 651 {
width: ac(342px, 300px, 360, 375);
height: ac(342px, 300px, 360, 375);
}
}
.wheel__wheel-item-icon,
.wheel__wheel-item-title {
@mixin media 951 {
opacity: 1;
}
}
.wheel__wheel-prize-box-wrap {
opacity: 1;
.wheel__wheel-box-top,
.wheel__wheel-box-bottom {
opacity: 1;
}
}
.wheel__wheel-prize-box-wrap-animation {
opacity: 1;
animation: shakeBox 400ms ease infinite;
@keyframes shakeBox {
0% {
transform: translate(-50%, -50%);
}
25% {
transform: translate(calc(-50% + 5px), calc(-50% + 5px)) rotate(5deg);
}
50% {
transform: translate(-50%, -50%) rotate(0deg);
}
75% {
transform: translate(calc(-50% - 5px), calc(-50% + 5px)) rotate(-5deg);
}
100% {
transform: translate(-50%, -50%);
}
}
}
}
.wheel-start {
--white: #ffffff;
--blue: #074b60;
--dark-blue: #002f42;
--cl-primary: #e63041;
--moonstone: #80a5b0;
position: relative;
padding: ac(13px, 11px) ac(30px, 20px);
font-family: var(--font-main);
font-weight: 800;
font-size: 14px;
line-height: 128.57%;
letter-spacing: 0.005em;
text-transform: uppercase;
color: var(--white);
border: 2px solid var(--cl-primary);
border-radius: 100px;
cursor: pointer;
background: var(--cl-primary);
transition: all 0.25s ease;
[role='status'] {
svg {
fill: var(--white);
}
}
&:hover {
background-color: var(--white);
color: var(--cl-primary);
[role='status'] {
svg {
fill: var(--cl-primary);
}
}
}
&.disabled,
&:disabled {
opacity: 0.8;
cursor: not-allowed;
}
}
.wheel-input {
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: flex-start;
width: 100%;
input {
font-size: 14px;
font-weight: 500;
line-height: 125.79%;
padding: 8px 0;
width: 100%;
height: 40px;
background: transparent;
border-radius: 0;
border-bottom: 2px solid var(--white);
}
}
// Import the 'confetti' function from the '@tsparticles/confetti' package
import { confetti } from '@tsparticles/confetti';
// Select the wheel element from the DOM
const wheel = document.querySelector('.wheel');
// Check if the wheel element exists
if (wheel) {
// For opening the wheel popup
const openBtn = document.querySelector('#openBtn');
openBtn.addEventListener('click', () => wheel.classList.add('is-show'));
// Show the wheel for the first page load
if (localStorage.getItem('wheel') !== 'true') {
wheel.classList.add('is-show');
localStorage.setItem('wheel', 'true');
}
// Select the close button within the wheel
const closeBtn = wheel.querySelector('.wheel__close-btn');
// Add an event listener to close the wheel when the close button is clicked
closeBtn.addEventListener('click', () => wheel.classList.remove('is-show'));
// Add an event listener to close the wheel when clicking outside the wheel area
wheel.addEventListener('click', ({ target }) => {
if (
target.classList.contains('wheel') ||
target.classList.contains('wheel__wrap') ||
target.classList.contains('wheel__content-wrap')
)
wheel.classList.remove('is-show');
});
// Define a function to open the wheel (used externally)
window.openBlackFridayPopup = function openBlackFridayPopup() {
wheel.classList.add('is-show');
};
// Select various elements within the wheel
const wheelItemsWrap = wheel.querySelector('.wheel__wheel-item-list');
const itemList = wheel.querySelectorAll('.wheel__wheel-item');
const startBtn = wheel.querySelector('.wheel-start');
const centerStartBtn = wheel.querySelector('.wheel__wheel-button');
const phoneNumberWrap = wheel.querySelector('#black-friday-phone-number');
const phoneNumberInput = phoneNumberWrap.querySelector('input');
const phoneNumberInfo = phoneNumberWrap.querySelector('.input-info');
const boxText = wheel.querySelector('.wheel__wheel-prize-text');
const prizeInfoText = wheel.querySelector('.wheel__info-prize-text');
const prizeImg = wheel.querySelector('.wheel__wheel-prize-bg');
// Add an input event listener to enable/disable start buttons based on phone number validity
phoneNumberInput.addEventListener('input', () => {
if (phoneNumberInfo.classList.contains('active-not-valid')) {
startBtn.disabled = true;
centerStartBtn.disabled = true;
} else {
startBtn.disabled = false;
centerStartBtn.disabled = false;
}
});
// Calculate sector details for positioning wheel items
const sectorPercent = 100 / itemList.length;
const sectorDegree = 360 / itemList.length;
const deviationDegree = 90 - sectorDegree;
const animationDelay = 5000;
// Calculate the positions and gradients for each wheel item
const positionCalculate = [...itemList].reduce(
(acc, item, idx) => {
acc.itemRotateResult.push(
acc.degree + sectorDegree / 2 - deviationDegree
);
acc.degree += sectorDegree;
acc.percent += sectorPercent;
const color = idx % 2 ? '#F9F8FD' : '#E7364C';
const gradientSector = `${color} 0, ${color} ${acc.percent}% ${
idx !== itemList.length - 1 ? ', ' : ''
}`;
acc.gradientResult += gradientSector;
return acc;
},
{
percent: 0,
degree: 0,
gradientResult: '',
itemRotateResult: [],
}
);
// Apply the calculated rotation to each wheel item
positionCalculate.itemRotateResult.forEach((item, idx) => {
itemList[idx].style.transform = `translateY(-50%) rotate(${item}deg)`;
});
// Apply the calculated gradients to the wheel background
wheelItemsWrap.style.background = `radial-gradient(circle closest-side, transparent 100%, white 0), conic-gradient(${positionCalculate.gradientResult})`;
// Rotate the wheel items wrap to adjust the starting position
wheelItemsWrap.style.transform = `rotate(${deviationDegree}deg)`;
// Define a function to animate the wheel
const animateWheel = async () => {
// Mock response data (replace with actual API call)
const response = {
card_content: 'Discount 20%',
id: 1,
prize_image: './img/black-friday-gift-bg.png',
title: 'Discount 20%',
};
// Handle errors in the response
if (response.error) {
phoneNumberInfo.classList.add('active');
phoneNumberInfo.classList.remove('active-valid');
phoneNumberInfo.classList.add('active-not-valid');
phoneNumberInfo.textContent = response.error;
return;
}
// Disable buttons and input during animation
startBtn.disabled = true;
centerStartBtn.disabled = true;
phoneNumberInput.disabled = true;
// Find the index of the selected wheel item
const giftItemIdx = [...itemList].findIndex(
(item) => item.dataset.id === String(response.id)
);
// Add classes for animation
wheel.classList.add('animating');
wheelItemsWrap.style.transition = `transform ease-out ${animationDelay}ms`;
// Update text and image based on the response
const titleHTML = formatFirstWordWithSpan(response.title);
boxText.innerHTML = titleHTML;
prizeInfoText.innerHTML = titleHTML;
prizeImg.src = response.prize_image;
// Apply rotation to the wheel items wrap for animation
wheelItemsWrap.classList.add('animate');
wheelItemsWrap.style.transform = `rotate(${
7 * 360 -
giftItemIdx * sectorDegree -
sectorDegree / 2 +
deviationDegree +
getRandomArbitrary(-sectorDegree / 4, sectorDegree / 4)
}deg)`;
// Select the confetti element
const confettiEl = document.querySelector('#confetti');
// Set timeouts for finishing animation and confetti effect
setTimeout(() => {
wheel.classList.add('wheel-rotate-finished');
wheel.classList.remove('animating');
}, animationDelay + 500);
setTimeout(() => {
const rect = confettiEl.getBoundingClientRect();
const defaults = {
spread: 360,
ticks: 100,
gravity: 0,
decay: 0.94,
position: {
x: ((rect.x + rect.width / 2) * 100) / window.innerWidth,
y: ((rect.y + rect.height / 2) * 100) / window.innerHeight,
},
startVelocity: 30,
shapes: ['circle', 'square', 'tringle', 'polygon'],
colors: ['e63041', 'ffffff'],
};
// Trigger confetti effects with different configurations
confetti({
...defaults,
particleCount: 350,
scalar: 0.5,
});
confetti({
...defaults,
particleCount: 200,
scalar: 1,
});
confetti({
...defaults,
particleCount: 30,
scalar: 1.5,
});
}, animationDelay + 1050);
};
// Add click event listeners to start the wheel animation
startBtn.addEventListener('click', animateWheel);
centerStartBtn.addEventListener('click', animateWheel);
// Timer
const timerWrap = wheel.querySelector('.wheel__info-end-timer');
const days = timerWrap.querySelector('#days');
const hours = timerWrap.querySelector('#hours');
const minutes = timerWrap.querySelector('#minutes');
const seconds = timerWrap.querySelector('#seconds');
// Extract end date from the timer element's dataset
const { endDate } = timerWrap.dataset;
// Update the timer display every second
const timerId = setInterval(() => {
// Check if the end date has passed
if (endDate - Date.now() <= 0) {
// Display zeros when the timer is complete
days.textContent = '00';
hours.textContent = '00';
minutes.textContent = '00';
seconds.textContent = '00';
// Clear the interval to stop the timer
return clearInterval(timerId);
}
// Calculate remaining time
const separatedDate = convertMilliseconds(endDate - Date.now());
// Update timer display
days.textContent = addLeadingZero(separatedDate.days);
hours.textContent = addLeadingZero(separatedDate.hours);
minutes.textContent = addLeadingZero(separatedDate.minutes);
seconds.textContent = addLeadingZero(separatedDate.seconds);
}, 1000);
}
// Utility functions for wheel logic
// Convert milliseconds to days, hours, minutes, and seconds
function convertMilliseconds(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const remainingHours = hours % 24;
const remainingMinutes = minutes % 60;
const remainingSeconds = seconds % 60;
return {
days: days,
hours: remainingHours,
minutes: remainingMinutes,
seconds: remainingSeconds,
};
}
// Add a leading zero to a number if it's a single digit
function addLeadingZero(number) {
return String(number).padStart(2, '0');
}
// Generate a random number within a specified range
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
// Format the first word in a string with a span element
function formatFirstWordWithSpan(inputString) {
const words = inputString.split(' ');
const firstWord = words.shift();
const restOfText = words.join(' ');
// Create a formatted string with the first word wrapped in a span
const formattedString = words.length
? `<span>${firstWord}</span><span class="accent">${restOfText}</span>`
: `<span class="accent">${firstWord}</span>`;
return formattedString;
}
Для роботи потрібно встановити бібліотеку @tsparticles/confetti за допомогою команди pnpm i @tsparticles/confetti
Нижче пройшовся по html-структурі і описав основні моменти (для js залишив коменти в коді):
Кнопка для відкриття попапу з колесом фортуни, не обов'язкова, функціонал відкриття можна вішати на будь що (кнопки, банери і тд)
Поле для вводу номера телефону
Кнопка для запуску колеса (яка знаходиться біля поля для вводу номера телефону)
Дата закінчення акції в форматі UNIX-timestamp
Сектор колеса фортуни, wheel__wheel-item-title - заголовок, wheel__wheel-item-icon - іконка
Кнопка в центрі колеса для запуску і відображення виграшу
Обгортка для конфеті