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

Дата закінчення акції в форматі UNIX-timestamp

Сектор колеса фортуни, wheel__wheel-item-title - заголовок, wheel__wheel-item-icon - іконка

Сектор колеса фортуни, wheel__wheel-item-title - заголовок, wheel__wheel-item-icon - іконка

Кнопка в центрі колеса для запуску і відображення виграшу

Кнопка в центрі колеса для запуску і відображення виграшу

Обгортка для конфеті

Обгортка для конфеті

Додатково прикріпив зображення, PCSS та JS архівом

wheel.zip

0