Skip to content

Commit

Permalink
feat: split PageContainer into RootAppLayout, useAuthRefresh, and AppBar
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-anderson committed Feb 20, 2024
1 parent ce8cf20 commit a00b3a7
Show file tree
Hide file tree
Showing 35 changed files with 974 additions and 678 deletions.
84 changes: 84 additions & 0 deletions src/components/AppBar/AppBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
withNavDecorator,
withAppStateInfoDecorator,
type AppStateInfoDecoratorArgs,
} from "@/../.storybook/decorators";
import { QUERIES } from "@/graphql/queries";
import { STATIC_MOCK_USERS } from "@/tests/mockItems/staticMockUsers";
import { AppBar } from "./AppBar";
import type { Meta, StoryObj } from "@storybook/react";

const meta = {
title: "Components/AppBar/AppBar",
component: AppBar,
decorators: [withNavDecorator, withAppStateInfoDecorator],
args: {
_mock_apollo_decorator_args: {
mocks: [
{
request: { query: QUERIES.MY_PROFILE },
result: {
data: {
myProfile: STATIC_MOCK_USERS.Guy_McPerson.profile,
},
},
},
],
},
},
} satisfies Meta<AppStateInfoDecoratorArgs>;

export default meta;

///////////////////////////////////////////////////////////
// STORIES

type Story = StoryObj<typeof meta>;

export const UserNotAuthenticated = {
args: {
_app_state_info_decorator_args: {
appState: {
isUserAuthenticated: false,
isAccountActive: false,
isConnectOnboardingComplete: false,
},
},
},
} satisfies Story;

export const InactiveSubscription = {
args: {
_app_state_info_decorator_args: {
appState: {
isUserAuthenticated: true,
isAccountActive: false,
isConnectOnboardingComplete: false,
},
},
},
} satisfies Story;

export const StripeConnectOnboardingIncomplete = {
args: {
_app_state_info_decorator_args: {
appState: {
isUserAuthenticated: true,
isAccountActive: true,
isConnectOnboardingComplete: false,
},
},
},
} satisfies Story;

export const FullyOnboardedActiveUser = {
args: {
_app_state_info_decorator_args: {
appState: {
isUserAuthenticated: true,
isAccountActive: true,
isConnectOnboardingComplete: true,
},
},
},
} satisfies Story;
Original file line number Diff line number Diff line change
@@ -1,46 +1,43 @@
import { lazy } from "react";
import { styled } from "@mui/material/styles";
import MuiAppBar from "@mui/material/AppBar";
import { avatarClasses } from "@mui/material/Avatar";
import { usePageLayoutContext } from "@app/PageLayoutContext/usePageLayoutContext";
import { AppBarLogoBtn } from "./AppBarLogoBtn";
import { DesktopAppBarMenu } from "./DesktopAppBarMenu";
import { MobileAppBarMenu } from "./MobileAppBarMenu";
import { ENV } from "@/app/env";
import { AppBarLogoButton } from "./AppBarLogoButton";
import { AppBarMenu } from "./AppBarMenu";
import { appBarElementIDs } from "./elementIDs";
import { useAppBarHeight } from "./helpers";
import type { CSSObject } from "@emotion/react";

const DevTools = lazy(() => import(/* webpackChunkName: "DevTools" */ "@/components/DevTools"));

/**
* Mui Material AppBar, with position "fixed".
*
* **Positioning:** To ensure components do not render behind AppBar, an offset is
* used to take up the same height and width. This positioning solution and its
* alternatives are described [here](https://mui.com/material-ui/react-app-bar/#fixed-placement).
* **Positioning:** To ensure components do not render behind AppBar, an offset is used
* to take up the same height and width. This positioning solution and its alternatives
* are described [here](https://mui.com/material-ui/react-app-bar/#fixed-placement).
*
* > Don't add `theme.mixins.toolbar` as recommended in the docs, it makes the
* Offset, and just sets min-height and breakpoints, but it breaks other styles.
* > Don't add `theme.mixins.toolbar` as recommended in the docs. It does make an Offset,
* but (1) it breaks other styles, and (2) all it does is set min-height and breakpoints.
*/
export const AppBar = () => {
const { isMobilePageLayout } = usePageLayoutContext();

return (
<>
<StyledMuiAppBar id={appBarElementIDs.root} position="fixed" elevation={0}>
<AppBarLogoBtn />
{isMobilePageLayout ? <MobileAppBarMenu /> : <DesktopAppBarMenu />}
</StyledMuiAppBar>
<div id={appBarElementIDs.fixedPositionOffset} />
</>
);
};
export const AppBar = () => (
<>
<StyledMuiAppBar id={appBarElementIDs.root} position="fixed" elevation={0}>
<AppBarLogoButton />
{ENV.IS_DEV && !ENV.IS_STORYBOOK && <DevTools />}
<AppBarMenu />
</StyledMuiAppBar>
<div id={appBarElementIDs.fixedPositionOffset} />
</>
);

export const appBarElementIDs = {
root: "appbar-root",
fixedPositionOffset: "appbar-fixed-position-offset",
};

const StyledMuiAppBar = styled(MuiAppBar)(({ theme }) => {
// Get --app-bar-height from CSS variable set in PageContainer
const appBarHeight = "var(--app-bar-height)";
const StyledMuiAppBar = styled(MuiAppBar)(({ theme: { palette, variables } }) => {
// Get height from the useAppBarHeight helper:
const appBarHeight = useAppBarHeight(variables);

// These styles are all the same for both AppBar and its sibling offset (see jsdoc)
const sharedStyles = {
const sharedStyles: CSSObject = {
width: "100%",
borderWidth: "0 0 1px 0",
borderStyle: "solid",
/* AppBar has two variants, Mobile and Desktop, the conditional rendering of which
Expand All @@ -49,37 +46,32 @@ const StyledMuiAppBar = styled(MuiAppBar)(({ theme }) => {
determine properties like height and width. Why not just use media queries for this?
Because aside from factoring in viewport dimensions, isMobilePageLayout also factors
in the user's browser as determined by `navigator.userAgent`. */
...(theme.variables.isMobilePageLayout
...(variables.isMobilePageLayout
? {
height: appBarHeight,
minHeight: appBarHeight,
maxHeight: appBarHeight,
padding: "1.5rem",
backgroundColor: "transparent",
backgroundColor: palette.background.default,
}
: {
height: appBarHeight,
minHeight: appBarHeight,
maxHeight: appBarHeight,
padding: "1rem 2rem",
padding: "1rem",
}),
};

return {
...sharedStyles,
width: "100%",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: "1.5rem",
borderWidth: "0 0 1px 0",
borderStyle: "solid",
borderColor: theme.palette.divider,

[`& .${avatarClasses.root}:hover`]: {
cursor: "pointer",
},
borderColor: palette.divider,

// Apply sharedStyles to the sibling offset div
[`& + #${appBarElementIDs.fixedPositionOffset}`]: {
Expand Down
44 changes: 44 additions & 0 deletions src/components/AppBar/AppBarLogoButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { styled } from "@mui/material/styles";
import Text, { typographyClasses } from "@mui/material/Typography";
import { Logo, brandingClassNames } from "@/components/Branding";
import { Anchor } from "@/components/Navigation/Anchor";
import { APP_PATHS } from "@/routes/appPaths";
import { isAuthenticatedStore } from "@/stores";

export const AppBarLogoButton = () => {
const isAuthenticated = isAuthenticatedStore.useSubToStore();

return (
<StyledDiv>
<Anchor href={isAuthenticated ? APP_PATHS.HOME : APP_PATHS.ROOT}>
<Logo />
<Text variant="h1">Fixit</Text>
</Anchor>
</StyledDiv>
);
};

const StyledDiv = styled("div")(({ theme: { palette, variables } }) => ({
display: "flex",
flexDirection: "row",
alignItems: "center",

"& > a": {
// Increase the anchor's default hover-opacity (0.7)
"&:hover": {
opacity: 0.75,
},

[`& > .${brandingClassNames.fixitLogoImg}`]: {
height: variables.isMobilePageLayout ? "2.75rem" : "2.25rem",
border: "1px solid white",
},

[`& > .${typographyClasses.root}`]: {
margin: "0 auto 0 0.5rem",
fontSize: "1.5rem",
fontWeight: 400,
color: palette.text.primary,
},
},
}));
15 changes: 15 additions & 0 deletions src/components/AppBar/AppBarMenu/AppBarMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { usePageLayoutContext } from "@/app/PageLayoutContext/usePageLayoutContext";
import { DesktopAppBarMenu } from "./DesktopAppBarMenu";
import { MobileAppBarMenu } from "./MobileAppBarMenu";

/**
* This component returns an AppBar menu component which is appropriate for the device/viewport.
*
* - When `isMobilePageLayout` is `true`: returns {@link MobileAppBarMenu}
* - When `isMobilePageLayout` is `false`: returns {@link DesktopAppBarMenu}
*/
export const AppBarMenu = () => {
const { isMobilePageLayout } = usePageLayoutContext();

return isMobilePageLayout ? <MobileAppBarMenu /> : <DesktopAppBarMenu />;
};
77 changes: 77 additions & 0 deletions src/components/AppBar/AppBarMenu/DarkModeSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { styled } from "@mui/material/styles";
import Switch, { switchClasses as muiSwitchClasses } from "@mui/material/Switch";
import Tooltip from "@mui/material/Tooltip";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/ModeNightSharp";
import { THEME_NAMES } from "@/app/ThemeProvider/themes";
import { themeStore } from "@/stores";
import { appBarMenuElementIDs } from "./elementIDs";

export const DarkModeSwitch = () => {
const currentTheme = themeStore.useSubToStore();

const handleChange = () => themeStore.toggle(currentTheme);

return (
<Tooltip title="Toggle dark mode">
<StyledSwitch
id={appBarMenuElementIDs.darkModeSwitch} // <-- Chrome logs a warning if this is not set
checked={currentTheme === THEME_NAMES.DARK}
checkedIcon={<DarkModeIcon />}
icon={<LightModeIcon />}
onChange={handleChange}
inputProps={{ "aria-label": "dark mode switch" }}
/>
</Tooltip>
);
};

const StyledSwitch = styled(Switch)(({ theme: { palette } }) => ({
width: "65px",
height: "34px",
padding: "7px",

[`& > .${muiSwitchClasses.switchBase}`]: {
height: "33px",
width: "33px",
paddingRight: "10px", // <-- Nudges LightModeIcon to the left a bit, not aligning for some reason.
transform: "translateX(6px)",
borderWidth: "2px",
borderStyle: "solid",
borderColor: palette.mode === "dark" ? "rgba(0, 0, 0, 0.7)" : "rgba(255, 255, 255, 0.7)",
opacity: 1,
backgroundColor: palette.primary.main,

"&:hover": {
opacity: 1,
backgroundColor: palette.primary.dark,
},

// WHEN CHECKED:
[`&.${muiSwitchClasses.checked}`]: {
opacity: 1,
transform: "translateX(30px)",
backgroundColor: palette.primary.dark,

"&:hover": {
opacity: 1,
backgroundColor: palette.primary.main,
},

// Styles applied to the `DarkModeIcon`:
"& > svg": {
position: "absolute",
top: "4px",
left: "1px",
height: "1.5rem",
width: "1.5rem",
color: palette.text.primary,
transform: "rotateZ(140deg)",
},
},
},

[`& .${muiSwitchClasses.track}`]: {
borderRadius: 10,
},
}));
Loading

0 comments on commit a00b3a7

Please sign in to comment.