impress-2020/src/app/GlobalHeader.js

281 lines
7.3 KiB
JavaScript
Raw Normal View History

2020-09-10 02:32:14 -07:00
import React from "react";
import {
Box,
Button,
HStack,
IconButton,
Menu,
MenuButton,
MenuList,
MenuItem,
useDisclosure,
useToast,
2020-12-25 09:08:33 -08:00
} from "@chakra-ui/react";
import { HamburgerIcon } from "@chakra-ui/icons";
2020-09-10 02:32:14 -07:00
import { Link, useLocation } from "react-router-dom";
import { ChevronLeftIcon } from "@chakra-ui/icons";
import Image from "next/image";
2020-09-10 02:32:14 -07:00
import useCurrentUser, {
useAuthModeFeatureFlag,
useLogout,
} from "./components/useCurrentUser";
import HomeLinkIcon from "./images/home-link-icon.png";
import { useAuth0 } from "@auth0/auth0-react";
2020-09-10 02:32:14 -07:00
function GlobalHeader() {
2020-09-10 02:32:14 -07:00
return (
<Box display="flex" alignItems="center" flexWrap="wrap">
<HomeLink marginRight="2" />
2020-09-10 02:32:14 -07:00
<Box marginLeft="auto">
<UserNavBarSection />
</Box>
</Box>
);
}
function HomeLink(props) {
const { pathname } = useLocation();
const isHomePage = pathname === "/";
2020-09-10 02:32:14 -07:00
return (
<Box
as={Link}
to="/"
display="flex"
alignItems="center"
role="group"
// HACK: When we're on the homepage, I want the title "Dress to Impress"
// to stay visible for transition, but I don't want it to be a
// click target. To do this, I constrain the size of the container,
// and also remove pointer events from the overflowing children.
maxWidth={isHomePage ? "32px" : "none"}
{...props}
2020-09-10 02:32:14 -07:00
>
<Box
flex="0 0 auto"
display="flex"
alignItems="center"
marginRight="2"
position="relative"
2020-09-10 02:32:14 -07:00
transition="all 0.2s"
opacity="0.8"
_groupHover={{ transform: "scale(1.1)", opacity: "1" }}
_groupFocus={{ transform: "scale(1.1)", opacity: "1" }}
2020-09-10 02:32:14 -07:00
>
<Box
position="absolute"
right="100%"
opacity={isHomePage ? "0" : "1"}
pointerEvents={isHomePage ? "none" : "all"}
transform={isHomePage ? "translateX(3px)" : "none"}
transition="all 0.2s"
>
<ChevronLeftIcon />
</Box>
<Box height="32px" borderRadius="lg" boxShadow="md" overflow="hidden">
<Image
src={HomeLinkIcon}
alt=""
width={32}
height={32}
layout="fixed"
/>
</Box>
<Box
height="2em"
width="2em"
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
borderRadius="lg"
transition="border 0.2s"
/>
2020-09-10 02:32:14 -07:00
</Box>
<Box
flex="0 0 auto"
fontFamily="Delicious"
fontWeight="600"
fontSize="2xl"
display={{ base: "none", sm: "block" }}
opacity={isHomePage ? "0" : "1"}
transition="all 0.2s"
marginRight="2"
pointerEvents={isHomePage ? "none" : "all"}
_groupHover={{ fontWeight: "900" }}
_groupFocus={{ fontWeight: "900" }}
>
Dress to Impress
</Box>
2020-09-10 02:32:14 -07:00
</Box>
);
}
function UserNavBarSection() {
const { isLoading, isLoggedIn, id, username } = useCurrentUser();
2020-09-10 02:32:14 -07:00
if (isLoading) {
return null;
}
if (isLoggedIn) {
2020-09-10 02:32:14 -07:00
return (
<HStack align="center" spacing="2">
{username && (
<Box fontSize="sm" textAlign="right">
Hi, {username}!
</Box>
)}
<NavLinksList>
{id && (
<NavLinkItem as={Link} to={`/user/${id}/lists`}>
Lists
</NavLinkItem>
)}
2021-01-04 00:13:37 -08:00
<NavLinkItem as={Link} to={`/your-outfits`}>
Outfits
</NavLinkItem>
<NavLinkItem as={Link} to="/modeling">
Modeling
</NavLinkItem>
<LogoutButton />
</NavLinksList>
2020-09-10 02:32:14 -07:00
</HStack>
);
} else {
return (
<HStack align="center" spacing="2">
<NavButton as={Link} to="/modeling">
2020-09-10 02:32:14 -07:00
Modeling
</NavButton>
<LoginButton />
</HStack>
);
}
2020-09-10 02:32:14 -07:00
}
function LoginButton() {
const authMode = useAuthModeFeatureFlag();
const { loginWithRedirect } = useAuth0();
const { isOpen, onOpen, onClose } = useDisclosure();
const onClick = () => {
if (authMode === "auth0") {
loginWithRedirect();
} else if (authMode === "db") {
onOpen();
} else {
throw new Error(`unexpected auth mode: ${JSON.stringify(authMode)}`);
}
};
return (
<>
<NavButton onClick={onClick}>Log in</NavButton>
{authMode === "db" && (
<React.Suspense fallback="">
<LoginModal isOpen={isOpen} onClose={onClose} />
</React.Suspense>
)}
</>
);
}
// I don't wanna load all these Chakra components as part of the bundle for
// every single page. Split it out!
const LoginModal = React.lazy(() => import("./components/LoginModal"));
function LogoutButton() {
const toast = useToast();
const [logout, { loading, error }] = useLogout();
React.useEffect(() => {
if (error != null) {
console.error(error);
toast({
title: "Oops, there was an error logging you out.",
description: "Reload the page and try again? Sorry about that!",
status: "warning",
duration: null,
isClosable: true,
});
}
}, [error, toast]);
return (
<NavLinkItem
onClick={() => logout({ returnTo: window.location.origin })}
// NOTE: The `isLoading` prop will only be relevant in the desktop case,
// where this renders as a NavButton. In the mobile case, the menu
// doesn't have a loading UI, and it closes when you click the
// button anyway. Not ideal, but fine for a simple quick action!
isLoading={loading}
>
Log out
</NavLinkItem>
);
}
const NavLinkTypeContext = React.createContext("button");
2020-11-03 19:36:48 -08:00
/**
* Renders the given <NavLinkItem /> children as a dropdown menu or as a list
* of buttons, depending on the screen size.
*
* It actually renders both, and shows/hides them by media query!
*/
function NavLinksList({ children }) {
return (
<>
<Box display={{ base: "block", md: "none" }}>
<Menu>
<MenuButton>
<NavButton icon={<HamburgerIcon />} />
</MenuButton>
<MenuList>
<NavLinkTypeContext.Provider value="menu">
{children}
</NavLinkTypeContext.Provider>
</MenuList>
</Menu>
</Box>
<HStack spacing="2" display={{ base: "none", md: "flex" }}>
<NavLinkTypeContext.Provider value="button">
{children}
</NavLinkTypeContext.Provider>
</HStack>
</>
);
}
function NavLinkItem(props) {
const navLinkType = React.useContext(NavLinkTypeContext);
if (navLinkType === "button") {
return <NavButton {...props} />;
} else if (navLinkType === "menu") {
return <MenuItem {...props} />;
} else {
throw new Error(`unexpected navLinkType: ${JSON.stringify(navLinkType)}`);
}
}
2020-09-10 02:32:14 -07:00
const NavButton = React.forwardRef(({ icon, ...props }, ref) => {
const Component = icon ? IconButton : Button;
// Opacity is in a separate Box, to avoid overriding the built-in Button
// hover/focus states.
return (
<Box
opacity="0.8"
_hover={{ opacity: "1" }}
_focusWithin={{ opacity: "1" }}
>
2020-09-10 02:32:14 -07:00
<Component size="sm" variant="outline" icon={icon} ref={ref} {...props} />
</Box>
);
});
export default GlobalHeader;