Day 8 - A marketing banner component for VannaCharm

A small but powerful marketing banner component for VannaCharm that uses local storage and authentication state to show one-time messages to users

Written by Chris on December 8th, 2025

This post is part of the 'The 12 Days of Full Stack Dev' series, an adventure through full stack fintech app development. Join me through the first 12 days of December as we develop a variety of new features, fixes, and components within the Full Stack Craft fintech family. For the full list of days and topics in this series, see the original post: The 12 Days of Full Stack Dev.

Any good web marketer knows you need as many "CTAs" as possible. Well, maybe not every marketer, but I do know that having a well placed marketing banner can help drive conversions, especially during special sales or events. We can combine a bit of usage of local storage, as well with user authentication state to create a simple but effective marketing banner component for VannaCharm.

All together, the component looks like this:

import * as React from 'react';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useUserTier } from '@/hooks/useUserTier';

// KEEP OLD IDS FOR LEGACY OF NOT SHOWING THEM! (examples here, we don't have any old ones yet)
const SOME_OLD_ID = 'SOME_OLD_ID';

// Current banner
const HOLIDAY_SALE_2025_BANNER_DISMISSED = 'HOLIDAY_SALE_2025_BANNER_DISMISSED';

// A sticky banner that will appear across any page on the site until dismissed (then it is dismissed forever)
export function MarketingBanner() {
  const [dismissed, setDismissed] = useState(false);
  const [isClosing, setIsClosing] = useState(false);
  const { tier, isLoading: isTierLoading } = useUserTier();

  useEffect(() => {
    // Ensure old ones are always dismissed
    localStorage.setItem(SOME_OLD_ID, 'true');

    // Current one - check if dismissed
    const isBannerDismissed = localStorage.getItem(HOLIDAY_SALE_2025_BANNER_DISMISSED);
    if (isBannerDismissed === 'true') {
      setDismissed(true);
    }
  }, []);

  const handleClose = () => {
    setIsClosing(true);
    localStorage.setItem(HOLIDAY_SALE_2025_BANNER_DISMISSED, 'true');
    // Wait for animation to complete before fully dismissing
    setTimeout(() => {
      setDismissed(true);
    }, 300);
  };

  // Don't render anything while loading tier info
  if (isTierLoading) {
    return null;
  }

  // Don't show to users who already have lifetime tiers (nothing to convert!)
  const hasLifetime = tier === 'starter-lifetime' || tier === 'premium-lifetime';
  if (hasLifetime) {
    return null;
  }

  // Don't show if already dismissed
  if (dismissed) {
    return null;
  }

  return (
    <div 
      className={`sticky top-0 bg-lime-400 text-black text-center py-3 px-4 z-[60] transition-all duration-300 ease-in-out ${
        isClosing ? '-mt-12 opacity-0' : 'mt-0 opacity-100'
      }`}
      role="alert"
    >
      <div className="max-w-7xl mx-auto flex justify-center items-center">
        <div className="text-sm sm:text-base font-medium pr-8">
          🎉 Holiday Sale!{' '}
          <Link 
            href="/pricing" 
            className="underline font-bold hover:text-gray-800 transition-colors"
          >
            Get 50% off lifetime access - our best deal of the year!
          </Link>
        </div>
        <button 
          type="button" 
          aria-label="Dismiss banner" 
          onClick={handleClose}
          className="absolute right-2 sm:right-4 top-1/2 -translate-y-1/2 p-2 hover:bg-lime-500 rounded transition-colors"
        >
          <svg 
            xmlns="http://www.w3.org/2000/svg" 
            className="h-5 w-5" 
            viewBox="0 0 20 20" 
            fill="currentColor"
          >
            <path 
              fillRule="evenodd" 
              d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" 
              clipRule="evenodd" 
            />
          </svg>
        </button>
      </div>
    </div>
  );
}

We render it over the nav, so it looks like this:

Our marketing banner in VannaCharm.
Our marketing banner in VannaCharm.

Note this is setup to be shown only once, if the user decides to dismiss it, it will never be shown again. This is done by storing a flag in local storage when the user clicks the dismiss button. Finally, we also check the user's subscription tier (using our existing useUserTier hook) to avoid showing the banner to users who already have lifetime access, since they don't need to be converted!

That's It!

-Chris

More posts:

footer-frame