impress-2020/src/app/GlobalHeader.js
Matchu db8fe9f3c2 Logout button for new auth mode
Hey hey, logging out works! The server side of this was easy, but I made a few refactors to support it well on the client, like `useLoginActions` becoming just `useLogout` lol, and updating how the nav menu chooses between buttons vs menu because I wanted `<LogoutButton />` to contain some state.

We also did good Apollo cache stuff to update the page after you log in or out! I think some fields that don't derive from `User`, like `Item.currentUserOwnsThis`, are gonna fail to update until you reload the page but like that's fine idk :p

There's a known bug where logging out on the Your Outfits page turns into an infinite loop situation, because it's trying to do Auth0 stuff but the login keeps failing to have any effect because we're in db mode! I'll fix that next.
2022-08-17 16:05:36 -07:00

280 lines
7.3 KiB
JavaScript

import React from "react";
import {
Box,
Button,
HStack,
IconButton,
Menu,
MenuButton,
MenuList,
MenuItem,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { HamburgerIcon } from "@chakra-ui/icons";
import { Link, useLocation } from "react-router-dom";
import { ChevronLeftIcon } from "@chakra-ui/icons";
import Image from "next/image";
import useCurrentUser, {
useAuthModeFeatureFlag,
useLogout,
} from "./components/useCurrentUser";
import HomeLinkIcon from "./images/home-link-icon.png";
import { useAuth0 } from "@auth0/auth0-react";
function GlobalHeader() {
return (
<Box display="flex" alignItems="center" flexWrap="wrap">
<HomeLink marginRight="2" />
<Box marginLeft="auto">
<UserNavBarSection />
</Box>
</Box>
);
}
function HomeLink(props) {
const { pathname } = useLocation();
const isHomePage = pathname === "/";
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}
>
<Box
flex="0 0 auto"
display="flex"
alignItems="center"
marginRight="2"
position="relative"
transition="all 0.2s"
opacity="0.8"
_groupHover={{ transform: "scale(1.1)", opacity: "1" }}
_groupFocus={{ transform: "scale(1.1)", opacity: "1" }}
>
<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"
/>
</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>
</Box>
);
}
function UserNavBarSection() {
const { isLoading, isLoggedIn, id, username } = useCurrentUser();
if (isLoading) {
return null;
}
if (isLoggedIn) {
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>
)}
<NavLinkItem as={Link} to={`/your-outfits`}>
Outfits
</NavLinkItem>
<NavLinkItem as={Link} to="/modeling">
Modeling
</NavLinkItem>
<LogoutButton />
</NavLinksList>
</HStack>
);
} else {
return (
<HStack align="center" spacing="2">
<NavButton as={Link} to="/modeling">
Modeling
</NavButton>
<LoginButton />
</HStack>
);
}
}
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");
/**
* 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)}`);
}
}
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" }}
>
<Component size="sm" variant="outline" icon={icon} ref={ref} {...props} />
</Box>
);
});
export default GlobalHeader;