loading
Just a moment.

Family Popover Menu

November 2024

import { AnimatePresence, motion } from 'framer-motion';
import React, { useState } from 'react';
import { useClickOutside } from '@/hook/useClickOutside';
import { useMediaQuery } from '@/hook/useMediaQuery';

interface IconProps {
  className?: string;
}

const FamilyPopoverMenu = () => {
  const refMenu = React.useRef<HTMLDivElement>(null);
  const [openMenu, setOpenMenu] = useState(false);

  const isScreenSizeSm = useMediaQuery('(max-width: 640px)');
  const duration = 0.2;
  const transition = { duration, ease: [0.32, 0.72, 0, 1] };

  const menuVariants = {
    open: {
      opacity: 1,
      width: isScreenSizeSm ? '100%' : '320px',
      height: 220,
      borderRadius: '16px',
      bottom: -44,
      transition,
    },
    closed: {
      bottom: 0,
      opacity: 1,
      width: '48px',
      height: 48,
      borderRadius: '50%',
      transition,
    },
  };

  const contentVariants = {
    open: { opacity: 1, scale: 1, transition },
    closed: { opacity: 0, scale: 1, transition },
  };

  const buttonVariants = {
    open: {
      opacity: 0,
      transition: { duration: duration / 2 },
    },
    closed: {
      opacity: 1,
      transition: { duration },
    },
  };

  const items = [
    { title: 'Settings', text: 'Adjust your preferences', icon: GearIcon },
    { title: 'Messages', text: 'View your messages', icon: MessagesIcon },
    { title: 'Favorites', text: 'Manage your favorites', icon: HeartIcon },
  ];

  useClickOutside<HTMLDivElement>(refMenu, () => setOpenMenu(false));

  return (
    <div className="relative mx-6 mb-16 flex h-[300px] w-full items-end justify-start">
      <AnimatePresence>
        {openMenu && (
          <motion.div
            className="absolute bottom-0 left-0 flex flex-col items-center overflow-hidden bg-white p-1 text-black"
            initial="closed"
            animate="open"
            exit="closed"
            variants={menuVariants}
            onClick={(e) => e.stopPropagation()}
            ref={refMenu}
          >
            <motion.ul variants={contentVariants} className="relative flex w-full flex-col space-y-1 pl-0 mb-0">
              {items.map((item, index) => (
                <li
                  key={index}
                  className="w-full select-none rounded-b-[4px] rounded-t-[4px] bg-white transition-transform first:rounded-t-[12px] last:rounded-b-[12px] active:scale-[0.98]"
                >
                  <div className="flex items-center py-3">
                    <div className="px-4">
                      <item.icon className="h-6 w-6 text-black" />
                    </div>
                    <div>
                      <h3 className="text-base text-black m-0 font-medium">{item.title}</h3>
                      <p className="text-sm text-black/50 m-0">{item.text}</p>
                    </div>
                  </div>
                </li>
              ))}
            </motion.ul>
          </motion.div>
        )}
      </AnimatePresence>
      <motion.button
        className="absolute bottom-0 left-0 flex h-12 w-12 items-center justify-center rounded-full outline-none bg-white text-black"
        disabled={openMenu}
        onClick={(e) => {
          e.stopPropagation();
          setOpenMenu(true);
        }}
        variants={buttonVariants}
        initial="closed"
        animate={openMenu ? 'open' : 'closed'}
        whileTap={{ scale: 0.95 }}
      >
        <PlusIcon className="h-7 w-7" />
      </motion.button>
    </div>
  );
};

const MessagesIcon = ({ className }: IconProps) => (
  <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
    <g fill="none" stroke="black">
      <rect width="16" height="12" x="4" y="6" rx="2" />
      <path d="m4 9l7.106 3.553a2 2 0 0 0 1.788 0L20 9" />
    </g>
  </svg>
);

const GearIcon = ({ className }: IconProps) => (
  <svg className={className} xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
    <g fill="none" stroke="black" strokeWidth="1.5">
      <circle cx="12" cy="12" r="3" />
      <path d="M13.765 2.152C13.398 2 12.932 2 12 2s-1.398 0-1.765.152a2 2 0 0 0-1.083 1.083c-.092.223-.129.484-.143.863a1.62 1.62 0 0 1-.79 1.353a1.62 1.62 0 0 1-1.567.008c-.336-.178-.579-.276-.82-.308a2 2 0 0 0-1.478.396C4.04 5.79 3.806 6.193 3.34 7s-.7 1.21-.751 1.605a2 2 0 0 0 .396 1.479c.148.192.355.353.676.555c.473.297.777.803.777 1.361s-.304 1.064-.777 1.36c-.321.203-.529.364-.676.556a2 2 0 0 0-.396 1.479c.052.394.285.798.75 1.605c.467.807.7 1.21 1.015 1.453a2 2 0 0 0 1.479.396c.24-.032.483-.13.819-.308a1.62 1.62 0 0 1 1.567.008c.483.28.77.795.79 1.353c.014.38.05.64.143.863a2 2 0 0 0 1.083 1.083C10.602 22 11.068 22 12 22s1.398 0 1.765-.152a2 2 0 0 0 1.083-1.083c.092-.223.129-.483.143-.863c.02-.558.307-1.074.79-1.353a1.62 1.62 0 0 1 1.567-.008c.336.178.579.276.819.308a2 2 0 0 0 1.479-.396c.315-.242.548-.646 1.014-1.453s.7-1.21.751-1.605a2 2 0 0 0-.396-1.479c-.148-.192-.355-.353-.676-.555A1.62 1.62 0 0 1 19.562 12c0-.558.304-1.064.777-1.36c.321-.203.529-.364.676-.556a2 2 0 0 0 .396-1.479c-.052-.394-.285-.798-.75-1.605c-.467-.807-.7-1.21-1.015-1.453a2 2 0 0 0-1.479-.396c-.24.032-.483.13-.82.308a1.62 1.62 0 0 1-1.566-.008a1.62 1.62 0 0 1-.79-1.353c-.014-.38-.05-.64-.143-.863a2 2 0 0 0-1.083-1.083Z" />
    </g>
  </svg>
);

const HeartIcon = (props: IconProps) => {
  return (
    <svg className={props.className} xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
      <path fill="black" fillRule="evenodd" d="M5.624 4.424C3.965 5.182 2.75 6.986 2.75 9.137c0 2.197.9 3.891 2.188 5.343c1.063 1.196 2.349 2.188 3.603 3.154q.448.345.885.688c.526.415.995.778 1.448 1.043s.816.385 1.126.385s.674-.12 1.126-.385c.453-.265.922-.628 1.448-1.043q.437-.344.885-.687c1.254-.968 2.54-1.959 3.603-3.155c1.289-1.452 2.188-3.146 2.188-5.343c0-2.15-1.215-3.955-2.874-4.713c-1.612-.737-3.778-.542-5.836 1.597a.75.75 0 0 1-1.08 0C9.402 3.882 7.236 3.687 5.624 4.424M12 4.46C9.688 2.39 7.099 2.1 5 3.059C2.786 4.074 1.25 6.426 1.25 9.138c0 2.665 1.11 4.699 2.567 6.339c1.166 1.313 2.593 2.412 3.854 3.382q.43.33.826.642c.513.404 1.063.834 1.62 1.16s1.193.59 1.883.59s1.326-.265 1.883-.59c.558-.326 1.107-.756 1.62-1.16q.396-.312.826-.642c1.26-.97 2.688-2.07 3.854-3.382c1.457-1.64 2.567-3.674 2.567-6.339c0-2.712-1.535-5.064-3.75-6.077c-2.099-.96-4.688-.67-7 1.399" clipRule="evenodd" />
    </svg>
  )
}

const PlusIcon = (props: IconProps) => {
  return (
    <svg className={props.className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
      <path fill="none" stroke="black" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 12h14m-7-7v14" />
    </svg>
  )
}


export default FamilyPopoverMenu;

// hook/useClickOutside.tsx
  //import type { RefObject } from 'react'
  // import { useEffect } from 'react';

  // type EventType =
  //   | 'mousedown'
  //   | 'mouseup'
  //   | 'touchstart'
  //   | 'touchend'
  //   | 'focusin'
  //   | 'focusout'

  // export function useClickOutside<T extends HTMLElement = HTMLElement>(
  //   ref: RefObject<T> | RefObject<T>[],
  //   handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
  //   eventType: EventType = 'mousedown',
  //   eventListenerOptions: AddEventListenerOptions = {},
  // ): void {
  //   useEffect(() => {
  //     if (typeof window === 'undefined') return;

  //     const listener = (event: MouseEvent | TouchEvent | FocusEvent) => {
  //       const target = event.target as Node;
  //       if (!target || !target.isConnected) return;

  //       const isOutside = Array.isArray(ref)
  //         ? ref.filter(r => Boolean(r.current)).every(r => r.current && !r.current.contains(target))
  //         : ref.current && !ref.current.contains(target);

  //       if (isOutside) {
  //         handler(event);
  //       }
  //     };

  //     window.addEventListener(eventType, listener, eventListenerOptions);
  //     return () => {
  //       window.removeEventListener(eventType, listener, eventListenerOptions);
  //     };
  //   }, [ref, handler, eventType, eventListenerOptions]);
  // }

  // hook/useMediaQuery.tsx
  //import React from "react";
  //import { useState, useCallback } from "react";

  //const useIsomorphicLayoutEffect =
    //typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;

  //export const useMediaQuery = (width: unknown) => {
    //const [targetReached, setTargetReached] = useState(false);

    //const updateTarget = useCallback((e: { matches: boolean | ((prevState: boolean) => boolean); }) => {
      //setTargetReached(e.matches);
    //}, []);

    //useIsomorphicLayoutEffect(() => {
    //  const media = window.matchMedia(`(max-width: ${width}px)`);
    //  media.addListener(updateTarget);

    //  if (media.matches) {
    //    setTargetReached(true);
    //  }

    //  return () => media.removeListener(updateTarget);
    //}, [updateTarget, width]);

    //return targetReached;
  //};