GitFOSS
.ts
TypeScript
(application/typescript)
// 3rd-party
import React, { useMemo, VFC } from "react";
import styled, { css } from "styled-components";
// app
import type { CommonProps, WithThemeSchemeProp } from "../types";
import { AppRoute } from "../routes.defs";
import { Const } from "../const";
import { NamedColors } from "../utils/style";
import { buildRouteLink } from "../utils/shared";
// app components
import { PageWrapper } from "./PageWrapper";
import { BurgerMenuIcon } from "./icons/BurgerMenuIcon";
import { MoonIcon } from "./icons/MoonIcon";
import { SunIcon } from "./icons/SunIcon";
import { PlusIcon } from "./icons/PlusIcon";

interface PageHeaderProps extends CommonProps {
  forceShowLogo?: boolean;
  setDrawerPrimaryOpen?: (predicate: (prev: boolean) => boolean) => void;
  setDrawerSettingsOpen?: (predicate: (prev: boolean) => boolean) => void;
}

export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
  commonProps,
  themeScheme,
  forceShowLogo = true,
  setDrawerPrimaryOpen = undefined,
  setDrawerSettingsOpen = undefined,
}) => {
  const invertThemeScheme = themeScheme === "light" ? "dark" : "light";

  const toggleDrawerPrimary = () => {
    if (setDrawerPrimaryOpen) {
      setDrawerPrimaryOpen((prev) => !prev);
    }
    if (setDrawerSettingsOpen) {
      setDrawerSettingsOpen((prev) => !prev);
    }
  };

  const pageHeaderActions = useMemo(() => {
    if (commonProps.authenticated) {
      return (
        <>
          <a
            aria-label={"View your profile and repositories"}
            title={`View @${commonProps.currentUserUsername || "ghost"} profile and settings`}
            href={buildRouteLink(
              AppRoute.USER_DETAILS,
              {
                username: commonProps.currentUserUsername || "ghost",
              },
              { encodeURIComponent: false },
            )}
          >
            <PageHeaderAvatar
              aria-label={commonProps.currentUserUsername || "ghost"}
              themeScheme={themeScheme}
              src={commonProps.currentUserAvatarUri || ""}
            />
          </a>
        </>
      );
    }

    return (
      <>
        <a
          aria-label={"Register a new account"}
          href={buildRouteLink(AppRoute.AUTH_REGISTER, null)}
        >
          Register
        </a>
        <a
          aria-label={"Login to your account"}
          href={buildRouteLink(AppRoute.AUTH_LOGIN, null)}
        >
          Login
        </a>
      </>
    );
  }, [commonProps.authenticated]);

  return (
    <StyledPageHeader themeScheme={themeScheme}>
      <PageWrapper style={{ gap: 12 }}>
        <StyledBurgerMenu
          themeScheme={themeScheme}
          onClick={toggleDrawerPrimary}
        >
          <BurgerMenuIcon
            color={NamedColors.TEXT_DEFAULT[themeScheme]}
            size={24}
          />
        </StyledBurgerMenu>
        <StyledLogoArea themeScheme={themeScheme} forceShowLogo={forceShowLogo}>
          <a href={"/"}>
            <h1>{Const.APP_NAME}</h1>
          </a>
        </StyledLogoArea>
        <StyledPageHeaderNav themeScheme={themeScheme}>
          <a
            aria-label={"Explore Repositories"}
            href={buildRouteLink(AppRoute.REPOSITORY_EXPLORE, null)}
            className={
              commonProps.path ===
              buildRouteLink(AppRoute.REPOSITORY_EXPLORE, null)
                ? "active"
                : undefined
            }
          >
            Explore
          </a>
          <a
            aria-label={"Contribute to GitFOSS development"}
            href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
              orgSlug: "ethicdevs",
              repoSlug: "gitfoss",
            })}
            className={
              commonProps.path!.startsWith(
                buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
                  orgSlug: "ethicdevs",
                  repoSlug: "gitfoss",
                }),
              )
                ? "active"
                : undefined
            }
          >
            Contribute
          </a>
        </StyledPageHeaderNav>
        <div style={{ flex: 1 }} />
        <StyledActionsArea>
          {commonProps.authenticated && (
            <a
              aria-label={"Create a new Repository"}
              title={"New Repository"}
              href={buildRouteLink(AppRoute.REPOSITORY_CREATE, null)}
            >
              <PlusIcon
                color={NamedColors.TEXT_DEFAULT[themeScheme]}
                size={24}
              />
            </a>
          )}
          <a
            data-smooth-scroll={"disabled"}
            aria-label={`Switch to ${invertThemeScheme} theme`}
            title={`Switch to ${invertThemeScheme} theme`}
            href={buildRouteLink(AppRoute.THEME_SET_SCHEME_ACTION, {
              themeScheme: invertThemeScheme,
            })}
          >
            {themeScheme === "light" ? (
              <MoonIcon
                color={NamedColors.TEXT_DEFAULT[themeScheme]}
                size={24}
              />
            ) : (
              <SunIcon
                color={NamedColors.TEXT_DEFAULT[themeScheme]}
                color2={NamedColors.BRAND_LINE[themeScheme]}
                size={24}
              />
            )}
          </a>
          {pageHeaderActions}
        </StyledActionsArea>
      </PageWrapper>
    </StyledPageHeader>
  );
};

const StyledBurgerMenu = styled.button<WithThemeSchemeProp>`
  ${({ themeScheme }) => css`
    display: flex;
    justify-content: center;
    align-items: center;

    /* above mobile size */
    @media only screen and (min-width: 768px) {
      & {
        display: none;
      }
    }

    width: 44px;
    min-width: 44px;
    max-width: 44px;
    height: 44px;
    min-height: 44px;
    max-height: 44px;

    padding-top: -2px;

    font-size: 20px;
    font-weight: thin;
    color: ${NamedColors.TEXT_MUTED[themeScheme]};
    background: ${NamedColors.HEADER_NAV_PILL[themeScheme]};
    border-image: none;
    border: none;
    border-radius: 22px;
    cursor: pointer;

    &.active,
    &:hover {
      color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
      background: ${NamedColors.CARD[themeScheme]};
    }

    &:active {
      font-size: 22px;
    }
  `}
`;

const StyledPageHeader = styled.header<WithThemeSchemeProp>`
  display: flex;
  flex-flow: row nowrap;
  justify-content: flex-start;
  align-items: center;

  height: 100%;
  width: 100%;

  /* above mobile size */
  @media only screen and (min-width: 768px) {
    & > ${PageWrapper} {
      padding: 0 8px;
    }
  }

  & > ${PageWrapper} {
    height: 100%;

    flex-flow: row nowrap;
    justify-content: flex-start;
    align-items: center;

    padding: 0;
    gap: 16px;
  }

  & a {
    transition: color 140ms ease-in-out 0s;
    text-decoration: none;

    ${({ themeScheme }) => css`
      color: ${NamedColors.TEXT_MUTED[themeScheme]};
    `};

    &:hover {
      ${({ themeScheme }) => css`
        color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
      `};
      text-decoration: underline;
    }
  }
`;

const StyledLogoArea = styled.div<
  WithThemeSchemeProp & { forceShowLogo: boolean }
>`
  ${({ forceShowLogo }) =>
    forceShowLogo !== true &&
    css`
      @media only screen and (min-width: 768px) {
        display: none;
      }
    `};

  @media only screen and (max-width: 768px) {
    & > a > h1 {
      font-size: 22px;
    }
  }

  & > a {
    display: flex;
    flex-flow: row nowrap;
    justify-content: flex-start;
    align-items: center;

    ${({ themeScheme }) => css`
      color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
    `};

    h1 {
      margin: 0;
    }
  }
`;

const StyledPageHeaderNav = styled.nav<WithThemeSchemeProp>`
  display: flex;
  flex-flow: row nowrap;
  justify-content: flex-start;
  align-items: center;

  /* flex: 1; */
  height: 40px;
  /* width: 100%; */

  gap: 2px;
  margin: 0;

  ${({ themeScheme }) => css`
    border-radius: 20px;
    background-color: ${NamedColors.HEADER_NAV_PILL[themeScheme]};
  `};

  @media only screen and (max-width: 768px) {
    display: none;
  }

  & > a {
    display: flex;
    justify-content: center;
    align-items: center;

    white-space: nowrap;

    height: 40px;
    min-width: 120px;
    padding: 0 16px;

    border-radius: 20px;
    font-weight: normal;
    text-decoration: none;

    ${({ themeScheme }) => css`
      color: ${NamedColors.TEXT_MUTED[themeScheme]};
      /* background-color: ${NamedColors.CARD[themeScheme]}; */
    `};

    &.active,
    &:hover {
      ${({ themeScheme }) => css`
        color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
        background-color: ${NamedColors.CARD[themeScheme]};
        font-weight: bold;
        font-family: monospace;
        text-decoration: none;
      `};
    }
  }
`;

const StyledActionsArea = styled.div`
  display: flex;
  flex-flow: row nowrap;
  justify-content: flex-end;
  align-items: center;

  & > a {
    margin-left: 12px;
    white-space: nowrap;
  }
`;

const PageHeaderAvatar = styled.img<WithThemeSchemeProp>`
  width: 40px;
  height: 40px;

  border-image: none;
  border-radius: 40px;

  ${({ themeScheme }) => css`
    border: 1px solid ${NamedColors.BORDER_CARD[themeScheme]};
    background-color: ${NamedColors.CARD_OVERLAY[themeScheme]};
  `};
`;

.ts
TypeScript
(application/typescript)
// 3rd-party
import React, { useMemo, VFC } from "react";
import styled, { css } from "styled-components";
// app
import type { CommonProps, WithThemeSchemeProp } from "../types";
import { AppRoute } from "../routes.defs";
import { Const } from "../const";
import { NamedColors } from "../utils/style";
import { buildRouteLink } from "../utils/shared";
// app components
import { PageWrapper } from "./PageWrapper";
import { BurgerMenuIcon } from "./icons/BurgerMenuIcon";
import { MoonIcon } from "./icons/MoonIcon";
import { SunIcon } from "./icons/SunIcon";
import { PlusIcon } from "./icons/PlusIcon";

interface PageHeaderProps extends CommonProps {
  forceShowLogo?: boolean;
  setDrawerPrimaryOpen?: (predicate: (prev: boolean) => boolean) => void;
  setDrawerSettingsOpen?: (predicate: (prev: boolean) => boolean) => void;
}

export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
  commonProps,
  themeScheme,
  forceShowLogo = true,
  setDrawerPrimaryOpen = undefined,
  setDrawerSettingsOpen = undefined,
}) => {
  const invertThemeScheme = themeScheme === "light" ? "dark" : "light";

  const toggleDrawerPrimary = () => {
    if (setDrawerPrimaryOpen) {
      setDrawerPrimaryOpen((prev) => !prev);
    }
    if (setDrawerSettingsOpen) {
      setDrawerSettingsOpen((prev) => !prev);
    }
  };

  const pageHeaderActions = useMemo(() => {
    if (commonProps.authenticated) {
      return (
        <>
          <a
            aria-label={"View your profile and repositories"}
            title={`View @${commonProps.currentUserUsername || "ghost"} profile and settings`}
            href={buildRouteLink(
              AppRoute.USER_DETAILS,
              {
                username: commonProps.currentUserUsername || "ghost",
              },
              { encodeURIComponent: false },
            )}
          >
            <PageHeaderAvatar
              aria-label={commonProps.currentUserUsername || "ghost"}
              themeScheme={themeScheme}
              src={commonProps.currentUserAvatarUri || ""}
            />
          </a>
        </>
      );
    }

    return (
      <>
        <a
          aria-label={"Register a new account"}
          href={buildRouteLink(AppRoute.AUTH_REGISTER, null)}
        >
          Register
        </a>
        <a
          aria-label={"Login to your account"}
          href={buildRouteLink(AppRoute.AUTH_LOGIN, null)}
        >
          Login
        </a>
      </>
    );
  }, [commonProps.authenticated]);

  return (
    <StyledPageHeader themeScheme={themeScheme}>
      <PageWrapper style={{ gap: 12 }}>
        <StyledBurgerMenu
          themeScheme={themeScheme}
          onClick={toggleDrawerPrimary}
        >
          <BurgerMenuIcon
            color={NamedColors.TEXT_DEFAULT[themeScheme]}
            size={24}
          />
        </StyledBurgerMenu>
        <StyledLogoArea themeScheme={themeScheme} forceShowLogo={forceShowLogo}>
          <a href={"/"}>
            <h1>{Const.APP_NAME}</h1>
          </a>
        </StyledLogoArea>
        <StyledPageHeaderNav themeScheme={themeScheme}>
          <a
            aria-label={"Explore Repositories"}
            href={buildRouteLink(AppRoute.REPOSITORY_EXPLORE, null)}
            className={
              commonProps.path ===
              buildRouteLink(AppRoute.REPOSITORY_EXPLORE, null)
                ? "active"
                : undefined
            }
          >
            Explore
          </a>
          <a
            aria-label={"Contribute to GitFOSS development"}
            href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
              orgSlug: "ethicdevs",
              repoSlug: "gitfoss",
            })}
            className={
              commonProps.path!.startsWith(
                buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
                  orgSlug: "ethicdevs",
                  repoSlug: "gitfoss",
                }),
              )
                ? "active"
                : undefined
            }
          >
            Contribute
          </a>
        </StyledPageHeaderNav>
        <div style={{ flex: 1 }} />
        <StyledActionsArea>
          {commonProps.authenticated && (
            <a
              aria-label={"Create a new Repository"}
              title={"New Repository"}
              href={buildRouteLink(AppRoute.REPOSITORY_CREATE, null)}
            >
              <PlusIcon
                color={NamedColors.TEXT_DEFAULT[themeScheme]}
                size={24}
              />
            </a>
          )}
          <a
            data-smooth-scroll={"disabled"}
            aria-label={`Switch to ${invertThemeScheme} theme`}
            title={`Switch to ${invertThemeScheme} theme`}
            href={buildRouteLink(AppRoute.THEME_SET_SCHEME_ACTION, {
              themeScheme: invertThemeScheme,
            })}
          >
            {themeScheme === "light" ? (
              <MoonIcon
                color={NamedColors.TEXT_DEFAULT[themeScheme]}
                size={24}
              />
            ) : (
              <SunIcon
                color={NamedColors.TEXT_DEFAULT[themeScheme]}
                color2={NamedColors.BRAND_LINE[themeScheme]}
                size={24}
              />
            )}
          </a>
          {pageHeaderActions}
        </StyledActionsArea>
      </PageWrapper>
    </StyledPageHeader>
  );
};

const StyledBurgerMenu = styled.button<WithThemeSchemeProp>`
  ${({ themeScheme }) => css`
    display: flex;
    justify-content: center;
    align-items: center;

    /* above mobile size */
    @media only screen and (min-width: 768px) {
      & {
        display: none;
      }
    }

    width: 44px;
    min-width: 44px;
    max-width: 44px;
    height: 44px;
    min-height: 44px;
    max-height: 44px;

    padding-top: -2px;

    font-size: 20px;
    font-weight: thin;
    color: ${NamedColors.TEXT_MUTED[themeScheme]};
    background: ${NamedColors.HEADER_NAV_PILL[themeScheme]};
    border-image: none;
    border: none;
    border-radius: 22px;
    cursor: pointer;

    &.active,
    &:hover {
      color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
      background: ${NamedColors.CARD[themeScheme]};
    }

    &:active {
      font-size: 22px;
    }
  `}
`;

const StyledPageHeader = styled.header<WithThemeSchemeProp>`
  display: flex;
  flex-flow: row nowrap;
  justify-content: flex-start;
  align-items: center;

  height: 100%;
  width: 100%;

  /* above mobile size */
  @media only screen and (min-width: 768px) {
    & > ${PageWrapper} {
      padding: 0 8px;
    }
  }

  & > ${PageWrapper} {
    height: 100%;

    flex-flow: row nowrap;
    justify-content: flex-start;
    align-items: center;

    padding: 0;
    gap: 16px;
  }

  & a {
    transition: color 140ms ease-in-out 0s;
    text-decoration: none;

    ${({ themeScheme }) => css`
      color: ${NamedColors.TEXT_MUTED[themeScheme]};
    `};

    &:hover {
      ${({ themeScheme }) => css`
        color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
      `};
      text-decoration: underline;
    }
  }
`;

const StyledLogoArea = styled.div<
  WithThemeSchemeProp & { forceShowLogo: boolean }
>`
  ${({ forceShowLogo }) =>
    forceShowLogo !== true &&
    css`
      @media only screen and (min-width: 768px) {
        display: none;
      }
    `};

  @media only screen and (max-width: 768px) {
    & > a > h1 {
      font-size: 22px;
    }
  }

  & > a {
    display: flex;
    flex-flow: row nowrap;
    justify-content: flex-start;
    align-items: center;

    ${({ themeScheme }) => css`
      color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
    `};

    h1 {
      margin: 0;
    }
  }
`;

const StyledPageHeaderNav = styled.nav<WithThemeSchemeProp>`
  display: flex;
  flex-flow: row nowrap;
  justify-content: flex-start;
  align-items: center;

  /* flex: 1; */
  height: 40px;
  /* width: 100%; */

  gap: 2px;
  margin: 0;

  ${({ themeScheme }) => css`
    border-radius: 20px;
    background-color: ${NamedColors.HEADER_NAV_PILL[themeScheme]};
  `};

  @media only screen and (max-width: 768px) {
    display: none;
  }

  & > a {
    display: flex;
    justify-content: center;
    align-items: center;

    white-space: nowrap;

    height: 40px;
    min-width: 120px;
    padding: 0 16px;

    border-radius: 20px;
    font-weight: normal;
    text-decoration: none;

    ${({ themeScheme }) => css`
      color: ${NamedColors.TEXT_MUTED[themeScheme]};
      /* background-color: ${NamedColors.CARD[themeScheme]}; */
    `};

    &.active,
    &:hover {
      ${({ themeScheme }) => css`
        color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
        background-color: ${NamedColors.CARD[themeScheme]};
        font-weight: bold;
        font-family: monospace;
        text-decoration: none;
      `};
    }
  }
`;

const StyledActionsArea = styled.div`
  display: flex;
  flex-flow: row nowrap;
  justify-content: flex-end;
  align-items: center;

  & > a {
    margin-left: 12px;
    white-space: nowrap;
  }
`;

const PageHeaderAvatar = styled.img<WithThemeSchemeProp>`
  width: 40px;
  height: 40px;

  border-image: none;
  border-radius: 40px;

  ${({ themeScheme }) => css`
    border: 1px solid ${NamedColors.BORDER_CARD[themeScheme]};
    background-color: ${NamedColors.CARD_OVERLAY[themeScheme]};
  `};
`;

GitFOSS • v0.2.0 (#421408f) • MIT License