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: var(--primary); *   --discount-font-family: var(--font-family-body); *   --discount-background: var(--primary-shadow); *   --discount-image-background: var(--contrast-100); *   --discount-text: var(--background); *   --discount-spinner-background: var(--background); *   --discount-spinner-text: var(--foreground); *   --discount-close-background: transparent; *   --discount-close-icon: var(--foreground); *   --discount-close-background-hover: color-mix(in oklab, var(--background) 40%, transparent); * } * ``` */export 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(        '@container relative top-0 left-0 flex h-dvh w-full items-center justify-center bg-(--discount-image-background,var(--contrast-100)) font-(family-name:--discount-font-family,var(--font-family-body)) text-(--discount-text,var(--background)) transition-[opacity,translate] duration-300',        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 top-5 right-5 flex h-8 w-8 items-center justify-center rounded-full bg-(--discount-close-background,transparent) text-(--discount-close-icon,var(--foreground)) transition-colors duration-300 hover:bg-(--discount-close-background-hover,color-mix(in_oklab,var(--background)_40%,transparent)) focus-visible:ring-2 focus-visible:ring-(--discount-focus,var(--foreground)) focus-visible:outline-hidden"        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-(--discount-background,var(--primary-shadow)) transition-transform focus-visible:ring-2 focus-visible:ring-(--button-focus,var(--primary)) focus-visible:ring-offset-2 focus-visible:outline-hidden 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 items-center justify-center bg-(--discount-background,var(--primary-shadow)) py-3 text-center text-2xl leading-none font-medium select-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-(--discount-spinner-background,var(--background)) px-6 pt-4 pb-6 @4xl:rounded-t-3xl @4xl:p-0">        <div className="relative h-[100px] w-full overflow-hidden bg-(--discount-spinner-background,var(--background)) text-(--discount-spinner-text,var(--foreground)) before:absolute before:top-0 before:left-0 before:z-10 before:h-8 before:w-full before:bg-gradient-to-b before:from-(--discount-spinner-background,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-(--discount-spinner-background,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 items-center justify-center py-1 text-5xl leading-[1] font-medium tracking-[-1px] text-(--discount-spinner-text,var(--foreground)) uppercase transition-transform duration-500 select-none @4xl:justify-end @4xl:px-6"                key={index}              >                {discount.label}              </div>            ))}          </div>        </div>        {renderButton === true && (          <Button            className="w-full justify-center select-none"            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://storage.googleapis.com/s.mkswft.com/RmlsZTpiMjMxMjAyZS05MDFjLTRjY2YtYWNiYS02ZmY1ZWQxZTVlZGQ=/electric-discount.jpeg"        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: var(--primary);  --discount-font-family: var(--font-family-body);  --discount-background: var(--primary-shadow);  --discount-image-background: var(--contrast-100);  --discount-text: var(--background);  --discount-spinner-background: var(--background);  --discount-spinner-text: var(--foreground);  --discount-close-background: transparent;  --discount-close-icon: var(--foreground);  --discount-close-background-hover: color-mix(in oklab, var(--background) 40%, transparent);}