Menu

Change language

GitHub repo

Flash Sale Offer Bar

blockecommerce

Limited-time flash sale countdown deal bar with stock claim progress meter, tiered multi-buy volume discounts, and 1-click cart activation.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/flash-sale-offer-bar.json
Manifest:https://uipkge.dev/r/lit/flash-sale-offer-bar.jsonInstalls to:src/uip/blocks/flash-sale-offer-bar/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
sale-titlestringLightning Deal: Limited Quantity Batchoptional
end-time-isostring—optional
claimed-percentnumber78optional
items-leftnumber14optional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

TierOffer
interface TierOffer {
  id: string
  quantity: number
  discountPct: number
  label: string
  badgeText?: string
  popular?: boolean
}
HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-flash-sale-offer-bar': UipFlashSaleOfferBar
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/flash-sale-offer-bar/flash-sale-offer-bar.ts10 kB
    import { LitElement, css, html, nothing } from 'lit'
    import { Flame, ShieldCheck, Tag } from 'lucide'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect imports: register the composed elements for standalone use
    // (consumers who install only this block still get working primitives).
    import '../../components/badge/badge'
    import '../../components/button/button'
    
    export interface TierOffer {
      id: string
      quantity: number
      discountPct: number
      label: string
      badgeText?: string
      popular?: boolean
    }
    
    const defaultTiers: TierOffer[] = [
      { id: 't1', quantity: 1, discountPct: 15, label: 'Standard Pack', badgeText: 'Save $45' },
      {
        id: 't2',
        quantity: 2,
        discountPct: 25,
        label: 'Duo Studio Bundle',
        badgeText: 'Save $150 · Best Value',
        popular: true,
      },
      { id: 't3', quantity: 3, discountPct: 35, label: 'Team / Lab Triad', badgeText: 'Save $315' },
    ]
    
    /**
     * <uip-flash-sale-offer-bar> — the registry FlashSaleOfferBar block as a web component.
     *
     * Class strings and tier data are React's verbatim. React state becomes
     * { state: true } fields; the 1s countdown interval is cleared in
     * disconnectedCallback. `tiers` is a property (attribute: false — set it from
     * JS). `onSelectTier` (tier) / `onClaimDeal` become `select-tier` /
     * `claim-deal` CustomEvents (bubbles, composed; tier in `detail`). React's
     * `currentTier` prop is declared but never read by the source, so it is not
     * ported.
     */
    export class UipFlashSaleOfferBar extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      static properties = {
        saleTitle: { attribute: 'sale-title' },
        endTimeIso: { attribute: 'end-time-iso' },
        claimedPercent: { type: Number, attribute: 'claimed-percent' },
        itemsLeft: { type: Number, attribute: 'items-left' },
        tiers: { attribute: false },
        selectedTierId: { state: true },
        hours: { state: true },
        minutes: { state: true },
        seconds: { state: true },
      }
    
      saleTitle = 'Lightning Deal: Limited Quantity Batch'
      endTimeIso?: string
      claimedPercent = 78
      itemsLeft = 14
      tiers: TierOffer[] = defaultTiers
    
      private selectedTierId = ''
      private hours = '06'
      private minutes = '42'
      private seconds = '19'
      private timer?: number
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'flash-sale-offer-bar')
        const list = this.tiers ?? []
        this.selectedTierId = list[1]?.id || list[0]?.id || ''
        this.updateCountdown()
        this.timer = window.setInterval(() => this.updateCountdown(), 1000)
      }
    
      disconnectedCallback() {
        super.disconnectedCallback()
        if (this.timer) window.clearInterval(this.timer)
      }
    
      willUpdate(changed: Map<string, unknown>) {
        if (changed.has('tiers')) {
          const list = this.tiers ?? []
          if (!list.some((t) => t.id === this.selectedTierId)) {
            this.selectedTierId = list[1]?.id || list[0]?.id || ''
          }
        }
      }
    
      private get targetTime(): string {
        if (this.endTimeIso) return this.endTimeIso
        return new Date(Date.now() + 6 * 3600 * 1000 + 42 * 60 * 1000 + 19 * 1000).toISOString()
      }
    
      private updateCountdown() {
        const target = new Date(this.targetTime).getTime()
        const now = Date.now()
        const diff = Math.max(0, target - now)
        const h = Math.floor(diff / (1000 * 60 * 60))
        const m = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
        const s = Math.floor((diff % (1000 * 60)) / 1000)
        this.hours = String(h).padStart(2, '0')
        this.minutes = String(m).padStart(2, '0')
        this.seconds = String(s).padStart(2, '0')
      }
    
      private handleSelect(tier: TierOffer) {
        this.selectedTierId = tier.id
        this.dispatchEvent(new CustomEvent('select-tier', { detail: { ...tier }, bubbles: true, composed: true }))
      }
    
      render() {
        const tiers = this.tiers ?? []
        return html`
          <div data-slot="flash-sale-offer-bar" class="border-border bg-card w-full overflow-hidden rounded-xl border shadow-xs">
            <div class="border-border bg-muted/40 flex flex-col gap-3 border-b p-4 sm:flex-row sm:items-center sm:justify-between">
              <div class="flex items-center gap-2.5">
                <div class="border-border bg-background flex size-8 items-center justify-center rounded-lg border shadow-2xs">
                  ${icon(Flame, 'flame', 'size-4 fill-orange-500/20 text-orange-500')}
                </div>
                <div>
                  <div class="flex items-center gap-2">
                    <span class="text-foreground text-sm font-semibold tracking-tight">${this.saleTitle}</span>
                    <uip-badge variant="destructive" class="h-5 gap-1 px-1.5 font-mono text-xs uppercase">
                      <span class="bg-background size-1.5 animate-pulse rounded-full"></span>
                      Live
                    </uip-badge>
                  </div>
                  <p class="text-muted-foreground text-xs">Price locks automatically once added to cart.</p>
                </div>
              </div>
    
              <div class="flex items-center gap-2 self-start sm:self-auto">
                <span class="text-muted-foreground text-xs font-medium">Ends in:</span>
                <div class="text-foreground flex items-center gap-1 font-mono text-xs font-semibold">
                  <span class="border-border bg-background rounded border px-1.5 py-0.5 shadow-2xs">${this.hours}h</span>
                  <span>:</span>
                  <span class="border-border bg-background rounded border px-1.5 py-0.5 shadow-2xs">${this.minutes}m</span>
                  <span>:</span>
                  <span class="border-border bg-background text-destructive rounded border px-1.5 py-0.5 shadow-2xs"
                    >${this.seconds}s</span
                  >
                </div>
              </div>
            </div>
    
            <div class="space-y-4 p-4">
              <div class="space-y-1.5">
                <div class="flex items-center justify-between text-xs">
                  <span class="text-foreground flex items-center gap-1.5 font-medium">
                    ${icon(Flame, 'flame', 'text-warning size-3.5')}
                    <span>${this.claimedPercent}% Claimed</span>
                  </span>
                  <span class="text-destructive font-mono font-medium">Only ${this.itemsLeft} units remaining in stock</span>
                </div>
                <div class="bg-muted h-2 w-full overflow-hidden rounded-full">
                  <div
                    class="h-full rounded-full bg-gradient-to-r from-orange-500 to-rose-500 transition-[width] duration-500"
                    style="width:${this.claimedPercent}%"
                  ></div>
                </div>
              </div>
    
              <div class="grid grid-cols-1 gap-2.5 sm:grid-cols-3">
                ${tiers.map(
                  (tier) => html`<button
                    type="button"
                    class=${`relative flex flex-col justify-between rounded-lg border p-3 text-left transition-colors ${
                      this.selectedTierId === tier.id
                        ? 'border-primary bg-primary/5 ring-primary ring-1'
                        : 'border-border bg-card hover:border-muted-foreground/40 hover:bg-muted/30'
                    }`}
                    @click=${() => this.handleSelect(tier)}
                  >
                    ${tier.popular
                      ? html`<span
                          class="border-border bg-primary text-primary-foreground absolute -top-2.5 right-3 rounded-full border px-2 py-0.5 text-xs font-semibold shadow-xs"
                        >
                          MOST POPULAR
                        </span>`
                      : nothing}
                    <div class="space-y-1">
                      <div class="flex items-center justify-between">
                        <span class="text-foreground text-xs font-semibold">
                          Buy ${tier.quantity} ${tier.quantity > 1 ? 'Units' : 'Unit'}
                        </span>
                        <uip-badge variant="secondary" class="text-success font-mono text-xs font-bold">
                          -${tier.discountPct}% OFF
                        </uip-badge>
                      </div>
                      <div class="text-muted-foreground text-xs font-medium">${tier.label}</div>
                    </div>
                    <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2 text-xs">
                      <span class="text-muted-foreground">${tier.badgeText}</span>
                      <div
                        class=${`flex size-4 items-center justify-center rounded-full border text-xs font-bold ${
                          this.selectedTierId === tier.id
                            ? 'border-primary bg-primary text-primary-foreground'
                            : 'border-muted-foreground/40'
                        }`}
                      >
                        ✓
                      </div>
                    </div>
                  </button>`,
                )}
              </div>
    
              <div class="border-border bg-muted/20 flex flex-col gap-2 rounded-lg border p-3 text-xs sm:flex-row sm:items-center sm:justify-between">
                <div class="text-muted-foreground flex items-center gap-2">
                  ${icon(ShieldCheck, 'shield-check', 'text-success size-4')}
                  <span>30-day price-match guarantee · Free express 2-day dispatch</span>
                </div>
                <uip-button
                  size="sm"
                  class="gap-1.5 text-xs shadow-xs"
                  @click=${() => this.dispatchEvent(new CustomEvent('claim-deal', { bubbles: true, composed: true }))}
                >
                  ${icon(Tag, 'tag', 'size-3.5')}
                  <span>Apply Tier Discount to Cart</span>
                </uip-button>
              </div>
            </div>
          </div>
        `
      }
    }
    
    customElements.get('uip-flash-sale-offer-bar') || customElements.define('uip-flash-sale-offer-bar', UipFlashSaleOfferBar)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-flash-sale-offer-bar': UipFlashSaleOfferBar
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/flash-sale-offer-bar.json