Discount

Installation

Add the following Soul components

The discount component uses the button component. Make sure you have added it to your project.

Install the following dependencies

npm install clsx lucide-react

Copy and paste the following code into your project

sections/discount/index.tsx

'use client';import { clsx } from 'clsx';import { X } from 'lucide-react';import Image from 'next/image';import { useCallback, useEffect, useState } from 'react';import { Button } from '@/vibes/soul/primitives/button';interface DiscountType {  label: string;  code: string;}export interface DiscountProps {  id: string;  backgroundImage: string;  discounts: DiscountType[];  onDismiss?: () => void;}/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css * :root { *   --discount-focus: hsl(var(--primary)); *   --discount-font-family: var(--font-family-body); *   --discount-background: color-mix(in oklab, hsl(var(--primary)), black 75%) *   --discount-image-background: hsl(var(--contrast-100)); *   --discount-text: hsl(var(--background)); *   --discount-spinner-background: hsl(var(--background)); *   --discount-spinner-text: hsl(var(--foreground)); *   --discount-close-background: transparent; *   --discount-close-icon: hsl(var(--foreground)/50%); *   --discount-close-background-hover: hsl(var(--background)/40%); * } * ``` */export const Discount = function Discount({  id,  backgroundImage,  discounts,  onDismiss,}: DiscountProps) {  const [dismissed, setDismissed] = useState(false);  const [initialized, setInitialized] = useState(false);  const [spin, setSpin] = useState(false);  const [isSpun, setIsSpun] = useState(false);  const [shuffledCodes, setShuffledCodes] = useState<DiscountType[]>([]);  const [copied, setCopied] = useState(false);  useEffect(() => {    const hidden = localStorage.getItem(`${id}-hidden-discount`) === 'true';    setInitialized(true);    setDismissed(hidden);  }, [id]);  useEffect(() => {    if (spin) {      setTimeout(() => {        setIsSpun(true);      }, 5000);    }  }, [spin]);  useEffect(() => {    const shuffled = shuffleCodes(      Array<DiscountType>(10)        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion        .fill(discounts[0]!)        .flatMap(() => discounts),    );    setShuffledCodes(shuffled);  }, [discounts]);  const hideDiscount = useCallback(() => {    setDismissed(true);    localStorage.setItem(`${id}-hidden-discount`, 'true');    onDismiss?.();  }, [id, onDismiss]);  const shuffleCodes = (array: DiscountType[]) => {    return array.sort(() => Math.random() - 0.5);  };  const copy = async () => {    try {      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion      await navigator.clipboard.writeText(shuffledCodes[shuffledCodes.length - 2]!.code);      setCopied(true);      setTimeout(() => {        setCopied(false);      }, 2000);    } catch (error) {      console.log('Failed to copy:', error);    }  };  if (!initialized) return null;  return (    <section      className={clsx(        'relative left-0 top-0 flex h-dvh w-full items-center justify-center bg-[var(--discount-image-background,hsl(var(--contrast-100)))] font-[family-name:var(--discount-font-family,var(--font-family-body))] text-[var(--discount-text,hsl(var(--background)))] transition-[opacity,transform] duration-300 @container',        dismissed ? 'translate-y-full opacity-0' : 'translate-y-0 opacity-100',      )}    >      <Image        alt="Background image"        className="object-cover"        fill        sizes="100vw"        src={backgroundImage}      />      <button        aria-label="Dismiss discount"        className="absolute right-5 top-5 flex h-8 w-8 items-center justify-center rounded-full bg-[var(--discount-close-background,transparent)] text-[var(--discount-close-icon,hsl(var(--foreground)))] transition-colors duration-300 hover:bg-[var(--discount-close-background-hover,hsl(var(--background)/40%))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--discount-focus,hsl(var(--foreground)))]"        onClick={(e) => {          e.preventDefault();          hideDiscount();        }}      >        <X absoluteStrokeWidth size={20} strokeWidth={1.5} />      </button>      {/* Desktop Version */}      <button        className="z-10 m-5 hidden h-24 w-full max-w-4xl cursor-pointer items-center justify-between gap-10 overflow-hidden rounded-3xl bg-[var(--discount-background,color-mix(in_oklab,hsl(var(--primary)),black_75%))] transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--button-focus,hsl(var(--primary)))] focus-visible:ring-offset-2 active:scale-[0.99] @4xl:flex"        onClick={() => {          if (isSpun) {            void copy();          } else {            setSpin(true);          }        }}      >        <DiscountUI          copied={copied}          copy={copy}          discounts={discounts}          isSpun={isSpun}          setSpin={setSpin}          shuffledCodes={shuffledCodes}          spin={spin}        />      </button>      {/* Mobile Version */}      <div className="z-10 m-5 flex w-full max-w-xs cursor-pointer flex-col items-center justify-between overflow-hidden rounded-3xl transition-transform @4xl:hidden">        <DiscountUI          copied={copied}          copy={copy}          discounts={discounts}          isSpun={isSpun}          renderButton          setSpin={setSpin}          shuffledCodes={shuffledCodes}          spin={spin}        />      </div>    </section>  );};interface DiscountUIProps {  isSpun: boolean;  copied: boolean;  spin: boolean;  setSpin: (value: boolean) => void;  discounts: DiscountType[];  shuffledCodes: DiscountType[];  copy: () => Promise<void>;  renderButton?: boolean;}const DiscountUI = ({  isSpun,  copied,  spin,  setSpin,  discounts,  shuffledCodes,  copy,  renderButton,}: DiscountUIProps) => {  let discountText = 'Spin for discount';  if (isSpun) {    discountText = copied ? 'Copied!' : `Copy discount code`;  }  return (    <>      <h2 className="flex min-h-20 w-full select-none items-center justify-center bg-[var(--discount-background,color-mix(in_oklab,hsl(var(--primary)),black_75%))] py-3 text-center text-2xl font-medium leading-none @4xl:mb-0 @4xl:justify-start @4xl:bg-transparent @4xl:px-6 @4xl:text-4xl">        {discountText}      </h2>      <div className="flex w-full max-w-xs flex-col gap-4 rounded-b-3xl bg-[var(--discount-spinner-background,hsl(var(--background)))] px-6 pb-6 pt-4 @4xl:rounded-t-3xl @4xl:p-0">        <div className="relative h-[100px] w-full overflow-hidden bg-[var(--discount-spinner-background,hsl(var(--background)))] text-[var(--discount-spinner-text,hsl(var(--foreground)))] before:absolute before:left-0 before:top-0 before:z-10 before:h-8 before:w-full before:bg-gradient-to-b before:from-[var(--discount-spinner-background,hsl(var(--background)))] before:to-transparent after:absolute after:bottom-0 after:left-0 after:z-10 after:h-8 after:w-full after:bg-gradient-to-t after:from-[var(--discount-spinner-background,hsl(var(--background)))] after:to-transparent @4xl:max-w-72">          <div            className="absolute -top-8 left-0 w-full transition-all [transition-duration:5000ms] [transition-timing-function:cubic-bezier(0.285,-0.125,0.050,1.130)]"            style={{              transform: spin                ? `translateY(calc(-100% + ${discounts.length * 33}px))`                : 'translateY(0)',            }}          >            {shuffledCodes.map((discount, index) => (              <div                className="flex select-none items-center justify-center py-1 text-5xl font-medium uppercase leading-[1] tracking-[-1px] text-[var(--discount-spinner-text,hsl(var(--foreground)))] transition-transform duration-500 @4xl:justify-end @4xl:px-6"                key={index}              >                {discount.label}              </div>            ))}          </div>        </div>        {renderButton === true && (          <Button            className="w-full select-none justify-center"            onClick={() => {              if (isSpun) {                void copy();              } else {                setSpin(true);              }            }}            variant="secondary"          >            {isSpun ? 'Copy' : 'Spin'}          </Button>        )}      </div>    </>  );};

Usage

'use client';import { Discount } from '@/vibes/soul/sections/discount';function Usage() {  return (      <Discount        backgroundImage="https://rstr.in/monogram/vibes/-ZKYHmBgOcN"        discounts={[          {            label: '20% off',            code: 'TAKE20',          },          {            label: '5% off',            code: 'TAKE5',          },        ]}        id="example-discount"      />  );}

API Reference

DiscountProps

PropTypeDefault
id*
string
backgroundImage*
string
discounts*
DiscountType[]
onDismiss
() => void

DiscountType

PropTypeDefault
label
string
code
string

CSS Variables

This component supports various CSS variables for theming. Here's a comprehensive list.

:root {  --discount-focus: hsl(var(--primary));  --discount-font-family: var(--font-family-body);  --discount-background: color-mix(in oklab, hsl(var(--primary)), black 75%);  --discount-image-background: hsl(var(--contrast-100));  --discount-text: hsl(var(--background));  --discount-spinner-background: hsl(var(--background));  --discount-spinner-text: hsl(var(--foreground));  --discount-close-background: transparent;  --discount-close-icon: hsl(var(--foreground) / 50%);  --discount-close-background-hover: hsl(var(--background) / 40%);}