sparkletown/sparkle

View on GitHub
src/components/molecules/TableHeader/TableHeader.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import React, { useCallback, useEffect, useMemo } from "react";
import {
  faChevronLeft,
  faLock,
  faLockOpen,
  faPen,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import firebase from "firebase/app";

import { MAX_TABLE_CAPACITY } from "settings";

import { unsetTableSeat } from "api/venue";

import { Table } from "types/Table";

import { experienceSelector } from "utils/selectors";
import { isTruthy } from "utils/types";

import { useSeatedTableUsers } from "hooks/useSeatedTableUsers";
import { useSelector } from "hooks/useSelector";
import { useShowHide } from "hooks/useShowHide";
import { useUser } from "hooks/useUser";

import { Toggler } from "components/atoms/Toggler";

import { EditTableTitleModal } from "./components/EditTableTitleModal";

import "./TableHeader.scss";

export interface TableHeaderProps {
  seatedAtTable: string;
  setSeatedAtTable: (val: string) => void;
  venueId: string;
  venueName: string;
  tables: Table[];
  defaultTables: Table[];
}

export const TableHeader: React.FC<TableHeaderProps> = ({
  seatedAtTable,
  setSeatedAtTable,
  venueId,
  venueName,
  tables,
  defaultTables,
}) => {
  const { userId, profile } = useUser();

  const { tables: allTables } = useSelector(experienceSelector) ?? {};
  const { isShown, show, hide } = useShowHide();

  const tableOfUser = useMemo(
    () =>
      seatedAtTable
        ? tables.find((table) => table.reference === seatedAtTable)
        : undefined,
    [seatedAtTable, tables]
  );

  // @debt This should be removed after the functions using it, are extracted into the api layer.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const firestoreUpdate = async (doc: string, update: any) => {
    const firestore = firebase.firestore();
    await firestore
      .doc(doc)
      .update(update)
      .catch(() => {
        firestore.doc(doc).set(update);
      });
  };

  const isCurrentTableLocked = isTruthy(!!allTables?.[seatedAtTable]?.locked);

  const [seatedTableUsers] = useSeatedTableUsers(venueId);
  const currentTableHasSeatedUsers = seatedTableUsers.some(
    (user) => user.path.tableReference === seatedAtTable
  );

  const tableTitle = tableOfUser?.title ?? "Table";
  const tableCapacity = tableOfUser?.capacity ?? MAX_TABLE_CAPACITY;
  const tableSubtitle = tableOfUser?.subtitle;

  // @debt This should be extracted into the api layer
  const setIsCurrentTableLocked = useCallback(
    (locked: boolean) => {
      const doc = `experiences/${venueName}`;
      const update = {
        tables: { ...allTables, [seatedAtTable]: { locked } },
      };
      void firestoreUpdate(doc, update);
    },
    [venueName, allTables, seatedAtTable]
  );

  const toggleIsCurrentTableLocked = useCallback(
    () => setIsCurrentTableLocked(!isCurrentTableLocked),
    [setIsCurrentTableLocked, isCurrentTableLocked]
  );

  useEffect(() => {
    if (isCurrentTableLocked && !currentTableHasSeatedUsers) {
      setIsCurrentTableLocked(false);
    }
  }, [
    seatedAtTable,
    isCurrentTableLocked,
    currentTableHasSeatedUsers,
    setIsCurrentTableLocked,
  ]);

  // @debt This should be extracted into the api layer
  const leaveSeat = useCallback(async () => {
    if (!userId || !profile) return;
    await unsetTableSeat(userId, { venueId });
    setSeatedAtTable("");
  }, [userId, profile, venueId, setSeatedAtTable]);

  useEffect(() => {
    window.addEventListener("beforeunload", leaveSeat);
    return () => {
      window.removeEventListener("beforeunload", leaveSeat, false);
    };
  }, [leaveSeat]);

  return (
    <div className="row TableHeader">
      <div className="TableHeader__leave-table">
        <button
          type="button"
          title={`Leave ${seatedAtTable}`}
          className="TableHeader__leave-button"
          onClick={leaveSeat}
        >
          <FontAwesomeIcon
            className="TableHeader__leave-button--icon"
            icon={faChevronLeft}
            size="xs"
          />
          Leave table
        </button>
      </div>

      <div className="TableHeader__topic-info">
        <div className="row TableHeader__topic">
          {tableTitle}

          <div className="TableHeader__edit-topic-button" onClick={show}>
            <FontAwesomeIcon icon={faPen} />
          </div>
        </div>

        {tableCapacity && (
          <span className="TableHeader__table-details">
            {tableOfUser?.subtitle && `${tableOfUser.subtitle} - `}
            {tableCapacity} seats
          </span>
        )}
      </div>

      <div className="TableHeader__lock-button">
        <FontAwesomeIcon
          className="TableHeader__lock-button--icon"
          icon={isCurrentTableLocked ? faLock : faLockOpen}
          size="sm"
        />
        <div className="TableHeader__lock-indication">
          {isCurrentTableLocked ? "Table Locked" : "Lock Table"}
        </div>
        {/* @debt pass the header into Toggler's 'label' prop instead of being external like this */}
        {/* @debt should this use 'toggled' instead of 'defaultToggled' to make it a controlled component? */}
        <Toggler
          containerClassName="TableHeader__lock-toggle"
          defaultToggled={isCurrentTableLocked}
          onChange={toggleIsCurrentTableLocked}
        />
      </div>

      <EditTableTitleModal
        title={tableTitle}
        subtitle={tableSubtitle}
        capacity={tableCapacity}
        defaultTables={defaultTables}
        onHide={hide}
        {...{ isShown, tables, tableOfUser }}
      />
    </div>
  );
};