Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make nav items stateful #345

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/shared/__tests__/navigation.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('navigation', () => {
isSidebarOpen={false}
openSidebar={jest.fn()}
navItems={defaultNavItems}
updateNavItems={jest.fn()}
></Navigation>
</MockThemeProvider>
</AuthProvider>,
Expand Down
74 changes: 36 additions & 38 deletions src/components/shared/layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,51 +29,54 @@ interface OwnProps {

type LayoutProps = OwnProps;

interface NavState {
interface NavVisibilityState {
isNavAtTop: boolean;
isNavVisible: boolean;
}

interface SidebarState {
isSidebarOpen: boolean;
interface NavItemsState {
navItems: NavItem[];
}

interface AuthState {
isUserAuthenticated: boolean;
interface SidebarState {
isSidebarOpen: boolean;
}

type LayoutState = NavState & SidebarState & AuthState;
type LayoutState = NavVisibilityState & SidebarState & NavItemsState;

const AUTH_IS = '[layout] AUTH_IS' as const;
const NAV_TOGGLE = '[layout] NAV_TOGGLE' as const;
const SIDEBAR_TOGGLE = '[layout] SIDEBAR_TOGGLE' as const;
const NAV_ITEMS_CHANGE = '[layout] NAV_ITEMS_CHANGE' as const;

type ActionType = typeof AUTH_IS | typeof SIDEBAR_TOGGLE | typeof NAV_TOGGLE;
type ActionType =
| typeof SIDEBAR_TOGGLE
| typeof NAV_TOGGLE
| typeof NAV_ITEMS_CHANGE;

interface LayoutAction<T = ActionType> {
type: T;
payload: NavState | SidebarState | AuthState;
payload: NavVisibilityState | SidebarState | NavItemsState;
}

const initialState: LayoutState = {
isNavAtTop: true,
isNavVisible: true,
navItems: [],
isSidebarOpen: false,
isUserAuthenticated: false,
};

const reducer: Reducer<LayoutState, LayoutAction> = (state, action) => {
switch (action.type) {
case NAV_TOGGLE:
case AUTH_IS:
case SIDEBAR_TOGGLE:
case NAV_ITEMS_CHANGE:
return { ...state, ...action.payload };
default:
throw new Error('unknown action type');
}
};

const Layout: FC<LayoutProps> = ({ children, navItems = defaultNavItems }) => {
const Layout: FC<LayoutProps> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);

useScrollPosition(
Expand Down Expand Up @@ -103,34 +106,28 @@ const Layout: FC<LayoutProps> = ({ children, navItems = defaultNavItems }) => {
const setOpen = (isSidebarOpen: boolean) => () =>
dispatch({ type: SIDEBAR_TOGGLE, payload: { isSidebarOpen } });

useLayoutEffect(() => {
const updateNavItems = () => {
const isUserAuthenticated = UserAuthHelper.isUserAuthenticated();
const newNavItems = defaultNavItems.filter(({ show }) => {
switch (show) {
case Show.Always:
return true;
case Show.AuthOnly:
return isUserAuthenticated;
case Show.GuestOnly:
return !isUserAuthenticated;
case Show.Never:
default:
return false;
}
});
dispatch({ type: NAV_ITEMS_CHANGE, payload: { navItems: newNavItems } });
};

dispatch({ type: AUTH_IS, payload: { isUserAuthenticated } });
useLayoutEffect(() => {
updateNavItems();
}, []);

// TODO: Possibly mutation to be added to useEffects
/*
https://reactjs.org/docs/hooks-reference.html#useeffect
Mutations, subscriptions, timers, logging, and other side effects are
not allowed inside the main body of a function component (referred to
as React’s render phase). Doing so will lead to confusing bugs and
inconsistencies in the UI.
*/
navItems = navItems.filter(({ show }) => {
switch (show) {
case Show.Always:
return true;
case Show.AuthOnly:
return state.isUserAuthenticated;
case Show.GuestOnly:
return !state.isUserAuthenticated;
case Show.Never:
default:
return false;
}
});

return (
<Fragment>
<Seo title="Home" />
Expand All @@ -141,9 +138,10 @@ const Layout: FC<LayoutProps> = ({ children, navItems = defaultNavItems }) => {
<Navigation
isAtTop={state.isNavAtTop}
isVisible={state.isNavVisible}
navItems={navItems}
navItems={state.navItems}
isSidebarOpen={state.isSidebarOpen}
openSidebar={setOpen(true)}
updateNavItems={updateNavItems}
/>

<ErrorBoundary
Expand All @@ -165,7 +163,7 @@ const Layout: FC<LayoutProps> = ({ children, navItems = defaultNavItems }) => {
onClose={setOpen(false)}
labelledby="menu-button"
>
<Sidebar navItems={navItems} />
<Sidebar navItems={state.navItems} />
</OffCanvas>
</Fragment>
</ThemeProvider>
Expand Down
17 changes: 8 additions & 9 deletions src/components/shared/navigation/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface OwnProps {
isSidebarOpen: boolean;
navItems?: NavItem[];
openSidebar: () => void;
updateNavItems: () => void;
}

type NavigationProps = OwnProps;
Expand Down Expand Up @@ -101,6 +102,7 @@ const Navigation: FC<NavigationProps> = ({
isSidebarOpen,
navItems = [],
openSidebar,
updateNavItems,
}) => {
const authContext = useContext(AuthContext);

Expand All @@ -109,18 +111,15 @@ const Navigation: FC<NavigationProps> = ({
return;
}
authContext.signOut();
updateNavItems();
navigate('/');
};

const setAvatar = (avatar: string) => {
navItems.map((navItem) => {
if (navItem.key == 'user-avatar-dropdown') {
navItem.item = <Profile content={avatar} signOut={signOut} />;
}
});
};

setAvatar(authContext.avatar);
navItems.map((navItem) => {
if (navItem.key == 'user-avatar-dropdown') {
navItem.item = <Profile content={authContext.avatar} signOut={signOut} />;
}
});

return (
<Wrapper isAtTop={isAtTop} isVisible={isAtTop || isVisible}>
Expand Down
32 changes: 20 additions & 12 deletions src/components/shared/navigation/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { navigate } from 'gatsby';
import React, { FC, useState, useRef } from 'react';
import React, { FC, useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';

import { UserAuthHelper } from '@helpers';
Expand Down Expand Up @@ -65,30 +65,38 @@ const DropdownItem = styled.div`

const Profile: FC<ProfileProps> = ({ content, isOnline = false, signOut }) => {
const [expanded, setExpanded] = useState(false);
const dropdownRef = useRef<HTMLDivElement | null>(null);

const close = () => {
const close = useCallback(() => {
setExpanded(false);
document.removeEventListener('click', close);
};

const setRef = (ref: HTMLDivElement | null) => {
dropdownRef.current = ref;
document.addEventListener('click', close);
};
}, []);

const toggle = () => setExpanded((expanded) => !expanded);
const goToProfile = () => navigate(`/profile/${UserAuthHelper.getUserId()}`);

useEffect(() => {
if (expanded) {
document.addEventListener('click', close);
return;
}
document.removeEventListener('click', close);
}, [expanded]);

return (
<Wrapper onClick={toggle}>
<Icon src={content} height={46} width={46} alt="profile image" />
{isOnline && <Dot src={dotIcon} height={16} width={16} alt="blue dot" />}

{expanded ? (
<Dropdown onBlur={close} ref={setRef}>
<Dropdown>
<DropdownItem onClick={goToProfile}>Profile</DropdownItem>
<DropdownItem onClick={signOut}>Sign Out</DropdownItem>
<DropdownItem
onClick={() => {
document.removeEventListener('click', close);
signOut();
}}
>
Sign Out
</DropdownItem>
</Dropdown>
) : null}
</Wrapper>
Expand Down
16 changes: 8 additions & 8 deletions src/mocks/responses/get-recent-devs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,31 @@ export const getRecentDevs: ApiResponse<RecentDev[]> = {
{
id: '08d6c5e7-6100-c770-61c3-834f6474a77b',
bio:
'Hi. Im Dan. Ive been working as a programmer for ~15 years now. Python, ML, NLP, server-side...',
"Hi. I'm Dan. I've been working as a programmer for ~15 years now. Python, ML, NLP, server-side...",
updatedAt: today,
},
{
id: '08d6c5e7-6100-c770-61c3-834f6474a77c',
bio:
'Hi. Im Dan. Ive been working as a programmer for ~15 years now. Python, ML, NLP, server-side...',
"Hi. I'm Dan. I've been working as a programmer for ~15 years now. Python, ML, NLP, server-side...",
updatedAt: oneDayAgo,
},
{
id: '08d6c5e7-6100-c770-61c3-834f6474a77c',
id: '08d6c5e7-6100-c770-61c3-834f6474a77d',
bio:
'Hi. Im Dan. Ive been working as a programmer for ~15 years now. Python, ML, NLP, server-side...',
"Hi. I'm Dan. I've been working as a programmer for ~15 years now. Python, ML, NLP, server-side...",
updatedAt: twoDaysAgo,
},
{
id: '08d6c5e7-6100-c770-61c3-834f6474a77d',
id: '08d6c5e7-6100-c770-61c3-834f6474a77e',
bio:
'Hi. Im Dan. Ive been working as a programmer for ~15 years now. Python, ML, NLP, server-side...',
"Hi. I'm Dan. I've been working as a programmer for ~15 years now. Python, ML, NLP, server-side...",
updatedAt: oneMonthAgo,
},
{
id: '08d6c5e7-6100-c770-61c3-834f6474a77d',
id: '08d6c5e7-6100-c770-61c3-834f6474a77f',
bio:
'Hi. Im Dan. Ive been working as a programmer for ~15 years now. Python, ML, NLP, server-side...',
"Hi. I'm Dan. I've been working as a programmer for ~15 years now. Python, ML, NLP, server-side...",
updatedAt: twoMonthsAgo,
},
],
Expand Down