All files / react-app/src/components/Banner banner.tsx

93.75% Statements 30/32
85.71% Branches 12/14
81.81% Functions 9/11
93.75% Lines 30/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103                                107x   37x               107x   107x 30x     107x       107x 26x 26x 26x   26x   26x 3x     26x 8x 8x 14x     8x 8x 8x         26x 18x 7x       26x         19x 1x 1x         19x     26x                                     14x    
import { Check, X } from "lucide-react";
import { useEffect, useRef } from "react";
import { useLocation } from "react-router";
 
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { Observer } from "@/utils/basic-observable";
 
import { Alert, AlertVariant } from "../Alert";
 
export type Banner = {
  header: string;
  body: string;
  variant?: AlertVariant;
  pathnameToDisplayOn: string;
};
 
class BannerObserver extends Observer<Banner> {
  create = (data: Banner) => {
    this.publish(data);
  };
 
  dismiss = () => {
    this.publish(null);
  };
}
 
const bannerState = new BannerObserver();
 
export const banner = (newBanner: Banner) => {
  return bannerState.create(newBanner);
};
 
export const dismissBanner = () => {
  return bannerState.dismiss();
};
 
export const Banner = () => {
  const bannerObserverRef = useRef<(() => void) | null>(null);
  const { pathname } = useLocation();
  const previousPathRef = useRef(pathname);
 
  const [activeBanner, setActiveBanner] = useLocalStorage<Banner | null>("banner", null);
 
  const onClose = () => {
    setActiveBanner(null);
  };
 
  useEffect(() => {
    Eif (bannerObserverRef.current === null) {
      bannerObserverRef.current = bannerState.subscribe((banner) => {
        setActiveBanner(banner);
      });
 
      return () => {
        bannerObserverRef.current?.();
        bannerObserverRef.current = null;
      };
    }
  }, [setActiveBanner]);
 
  useEffect(() => {
    if (activeBanner) {
      bannerState.create(activeBanner);
    }
  }, [activeBanner]);
 
  useEffect(() => {
    // only run cleanup if:
    // 1. we've actually navigated (pathname changed from previous render)
    // 2. there's an active banner to clean up
    // 3. the banner's target pathname doesn't match where we navigated to
    if (pathname !== previousPathRef.current) {
      Eif (activeBanner && activeBanner.pathnameToDisplayOn !== pathname) {
        onClose();
      }
    }
 
    // store current pathname for next render's comparison
    previousPathRef.current = pathname;
  }, [pathname, activeBanner]); // eslint-disable-line react-hooks/exhaustive-deps
 
  if (activeBanner && activeBanner.pathnameToDisplayOn === pathname) {
    return (
      <Alert variant={activeBanner.variant} className="mt-4 mb-8 flex-row text-sm">
        <div className="flex items-start justify-between">
          <Check />
          <div className="ml-2 w-full">
            <h3 className="text-lg font-bold" data-testid="banner-header">
              {activeBanner.header}
            </h3>
            <p data-testid="banner-body">{activeBanner.body}</p>
          </div>
          <button onClick={onClose} data-testid="banner-close">
            <X size={20} />
          </button>
        </div>
      </Alert>
    );
  }
 
  return null;
};