hyper-tuner/hyper-tuner-cloud

View on GitHub
src/components/TopBar.tsx

Summary

Maintainability
F
3 days
Test Coverage
import {
  CarOutlined,
  CloudDownloadOutlined,
  CloudUploadOutlined,
  DesktopOutlined,
  DownOutlined,
  FileExcelOutlined,
  FileTextOutlined,
  FileZipOutlined,
  FundOutlined,
  InfoCircleOutlined,
  LineChartOutlined,
  LoginOutlined,
  LogoutOutlined,
  SearchOutlined,
  SettingOutlined,
  SlidersOutlined,
  ToolOutlined,
  UserAddOutlined,
  UserOutlined,
} from '@ant-design/icons';
import { Button, Col, Dropdown, Grid, Layout, Radio, Row, Space, Tooltip, Typography } from 'antd';
import { useKBar } from 'kbar';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { Link, generatePath, useLocation, useMatch, useNavigate } from 'react-router-dom';
import { Collections } from '../@types/pocketbase-types';
import { useAuth } from '../contexts/AuthContext';
import useDb from '../hooks/useDb';
import useServerStorage from '../hooks/useServerStorage';
import { logOutSuccessful } from '../pages/auth/notifications';
import { removeFilenameSuffix } from '../pocketbase';
import { Routes } from '../routes';
import store from '../store';
import { TuneDataState } from '../types/state';
import { isMac } from '../utils/env';
import { isToggleSidebar } from '../utils/keyboard/shortcuts';
import { buildHyperTunerAppLink } from '../utils/url';

const { Header } = Layout;
const { useBreakpoint } = Grid;

const logsExtensionsIcons: Record<string, React.JSX.Element> = {
  mlg: <FileZipOutlined />,
  msl: <FileTextOutlined />,
  csv: <FileExcelOutlined />,
};

const TopBar = ({ tuneData }: { tuneData: TuneDataState | null }) => {
  const { xs, sm, lg } = useBreakpoint();
  const { pathname } = useLocation();
  const { currentUser, logout } = useAuth();
  const navigate = useNavigate();
  const { query } = useKBar();
  const buildTuneUrl = useCallback(
    (route: string) => (tuneData?.tuneId ? generatePath(route, { tuneId: tuneData.tuneId }) : null),
    [tuneData?.tuneId],
  );
  const hubPathMatch = useMatch(Routes.HUB);
  const tuneRootMatch = useMatch(`${Routes.TUNE_ROOT}/*`);
  const tuneTuneMatch = useMatch(`${Routes.TUNE_TUNE}/*`);
  const tuneLogMatch = useMatch(`${Routes.TUNE_LOGS}/*`);
  const toothLogMatch = useMatch(`${Routes.TUNE_DIAGNOSE}/*`);
  const tabMatch = useMatch(`${Routes.TUNE_TAB}/*`);
  const uploadMatch = useMatch(Routes.UPLOAD);
  const hubMatch = useMatch(Routes.HUB);
  const { downloadFile } = useServerStorage();
  const { getIni } = useDb();
  const downloadAnchorRef = useRef<HTMLAnchorElement | null>(null);
  const logoutClick = useCallback(() => {
    logout();
    logOutSuccessful();
    navigate(0);
  }, [logout, navigate]);

  const toggleCommandPalette = useCallback(() => {
    query.toggle();
  }, [query]);

  const handleGlobalKeyboard = useCallback((e: KeyboardEvent) => {
    if (isToggleSidebar(e)) {
      e.preventDefault();
      store.dispatch({ type: 'ui/toggleSidebar' });
    }
  }, []);

  const downloadLogsItems = {
    label: 'Logs',
    icon: <LineChartOutlined />,
    key: 'logs',
    children: (tuneData?.logFiles || []).map((filename) => ({
      key: filename,
      label: removeFilenameSuffix(filename),
      icon: logsExtensionsIcons[filename.slice(-3)],
      onClick: () =>
        downloadFile(Collections.Tunes, tuneData!.id, filename, downloadAnchorRef.current!),
    })),
  };

  const downloadToothLogsItems = {
    label: 'Tooth logs',
    icon: <SettingOutlined />,
    key: 'toothLogs',
    children: (tuneData?.toothLogFiles || []).map((filename) => ({
      key: filename,
      label: removeFilenameSuffix(filename),
      icon: logsExtensionsIcons[filename.slice(-3)],
      onClick: () =>
        downloadFile(Collections.Tunes, tuneData!.id, filename, downloadAnchorRef.current!),
    })),
  };

  const downloadItems = [
    {
      label: 'Open in app',
      icon: <DesktopOutlined />,
      key: 'open',
      disabled: false,
      onClick: () => window.open(buildHyperTunerAppLink(tuneData!.tuneId)),
    },
    {
      label: 'Tune',
      icon: <SlidersOutlined />,
      key: 'tune',
      children: [
        {
          label: 'Download',
          icon: <FileTextOutlined />,
          key: 'download',
          onClick: () =>
            downloadFile(
              Collections.Tunes,
              tuneData!.id,
              tuneData!.tuneFile,
              downloadAnchorRef.current!,
            ),
        },
      ],
    },
    {
      label: 'INI',
      icon: <FileTextOutlined />,
      key: 'ini',
      onClick: async () => {
        if (tuneData?.customIniFile) {
          downloadFile(
            Collections.Tunes,
            tuneData.id,
            tuneData.customIniFile,
            downloadAnchorRef.current!,
          );
        } else {
          const ini = await getIni(tuneData!.signature);
          downloadFile(Collections.IniFiles, ini!.id, ini!.file, downloadAnchorRef.current!);
        }
      },
    },
    (tuneData?.logFiles || []).length > 0 ? { ...downloadLogsItems } : null,
    (tuneData?.toothLogFiles || []).length > 0 ? { ...downloadToothLogsItems } : null,
  ];

  useEffect(() => {
    document.addEventListener('keydown', handleGlobalKeyboard);

    return () => {
      document.removeEventListener('keydown', handleGlobalKeyboard);
    };
  }, [currentUser, handleGlobalKeyboard]);

  const tabs = useMemo(
    () => (
      <Col span={16} md={16} sm={16}>
        <Radio.Group
          key={pathname}
          defaultValue={
            tuneLogMatch?.pathnameBase ||
            toothLogMatch?.pathnameBase ||
            tuneTuneMatch?.pathnameBase ||
            tabMatch?.pathname ||
            tuneRootMatch?.pathname ||
            hubPathMatch?.pathname
          }
          optionType="button"
          buttonStyle="solid"
          onChange={(e) => {
            navigate(e.target.value as string);
          }}
        >
          <Radio.Button value={buildTuneUrl(Routes.HUB)}>
            <Space>
              <CarOutlined />
              {lg && 'Hub'}
            </Space>
          </Radio.Button>
          <Radio.Button value={buildTuneUrl(Routes.TUNE_ROOT)}>
            <Space>
              <InfoCircleOutlined />
              {lg && 'Info'}
            </Space>
          </Radio.Button>
          <Radio.Button value={buildTuneUrl(Routes.TUNE_TUNE)}>
            <Space>
              <ToolOutlined />
              {lg && 'Tune'}
            </Space>
          </Radio.Button>
          <Radio.Button
            value={buildTuneUrl(Routes.TUNE_LOGS)}
            disabled={(tuneData?.logFiles || []).length === 0}
          >
            <Space>
              <FundOutlined />
              {lg && 'Logs'}
            </Space>
          </Radio.Button>
          <Radio.Button
            value={buildTuneUrl(Routes.TUNE_DIAGNOSE)}
            disabled={(tuneData?.toothLogFiles || []).length === 0}
          >
            <Space>
              <SettingOutlined />
              {lg && 'Diagnose'}
            </Space>
          </Radio.Button>
        </Radio.Group>
      </Col>
    ),
    [
      pathname,
      tuneLogMatch?.pathnameBase,
      toothLogMatch?.pathnameBase,
      tuneTuneMatch?.pathnameBase,
      tabMatch?.pathname,
      tuneRootMatch?.pathname,
      hubPathMatch?.pathname,
      buildTuneUrl,
      lg,
      tuneData?.logFiles,
      tuneData?.toothLogFiles,
      navigate,
    ],
  );

  const rightMenuColProps = tuneData?.tuneId
    ? {
        span: 8,
        md: 8,
        sm: 8,
      }
    : {
        span: 14,
        md: 10,
        sm: 8,
      };

  const downloadButton = useMemo(() => {
    const list = [];

    if (lg) {
      list.push(<span key="download-text">Download</span>);
    }

    if (sm) {
      list.push(<DownOutlined key="download-icon" />);
    }

    return list.length ? list : null;
  }, [lg, sm]);

  const userAuthMenuItems = useMemo(() => {
    return currentUser
      ? [
          {
            key: 'profile',
            icon: <UserOutlined />,
            label: 'Profile',
            onClick: () => {
              navigate(Routes.PROFILE);
            },
          },
          {
            key: 'logout',
            icon: <LogoutOutlined />,
            label: 'Logout',
            onClick: logoutClick,
          },
        ]
      : [
          {
            key: 'login',
            icon: <LoginOutlined />,
            label: 'Login',
            onClick: () => {
              navigate(Routes.LOGIN);
            },
          },
          {
            key: 'sign-up',
            icon: <UserAddOutlined />,
            label: 'Sign Up',
            onClick: () => {
              navigate(Routes.SIGN_UP);
            },
          },
        ];
  }, [currentUser, logoutClick, navigate]);

  const userMenuItems = [
    ...userAuthMenuItems,
    {
      key: 'divider',
      type: 'divider',
    },
    {
      key: 'about',
      icon: <InfoCircleOutlined />,
      label: 'About',
      onClick: () => {
        navigate(Routes.ABOUT);
      },
    },
  ];

  return (
    <Header className="app-top-bar" style={xs ? { padding: '0 5px' } : {}}>
      <Row>
        {tuneData?.tuneId ? (
          tabs
        ) : (
          <Col span={10} md={14} sm={16}>
            <Link to={Routes.HUB}>
              <Button icon={<CarOutlined />} type={hubMatch ? 'primary' : 'default'}>
                Hub
              </Button>
            </Link>
          </Col>
        )}
        <Col {...rightMenuColProps} style={{ textAlign: 'right' }}>
          <Space>
            {sm && (
              <Tooltip
                title={
                  <>
                    <Typography.Text keyboard>{isMac ? '⌘' : 'CTRL'}</Typography.Text>
                    <Typography.Text keyboard>K</Typography.Text>
                  </>
                }
              >
                <Button icon={<SearchOutlined />} onClick={toggleCommandPalette} />
              </Tooltip>
            )}
            <Link to={Routes.UPLOAD}>
              <Button icon={<CloudUploadOutlined />} type={uploadMatch ? 'primary' : 'default'}>
                {lg && 'Upload'}
              </Button>
            </Link>
            {tuneData?.tuneId && (
              <Dropdown menu={{ items: downloadItems }} placement="bottom" trigger={['click']}>
                <Button icon={<CloudDownloadOutlined />}>{downloadButton}</Button>
              </Dropdown>
            )}
            <Dropdown menu={{ items: userMenuItems }} placement="bottomRight" trigger={['click']}>
              <Button icon={<UserOutlined />}>{sm && <DownOutlined />}</Button>
            </Dropdown>
            {/* dummy anchor for file download */}
            <a href="#download" ref={downloadAnchorRef} style={{ display: 'none' }}>
              download
            </a>
          </Space>
        </Col>
      </Row>
    </Header>
  );
};

export default TopBar;