Setup new left navigation system

This commit is contained in:
Jackson Harper 2024-06-06 13:24:48 +08:00
parent 648cbe18cf
commit 2123b248c9
6 changed files with 368 additions and 149 deletions

View file

@ -0,0 +1,199 @@
import { PageMetaData, PageMetaDataProps } from '../patterns/PageMetaData'
import { Box, VStack } from '../elements/LayoutPrimitives'
import { ReactNode, useEffect, useState, useCallback } from 'react'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
import { useKeyboardShortcuts } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { NextRouter, useRouter } from 'next/router'
import { ConfirmationModal } from '../patterns/ConfirmationModal'
import { KeyboardShortcutListModal } from './KeyboardShortcutListModal'
import { setupAnalytics } from '../../lib/analytics'
import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
import { logout } from '../../lib/logout'
import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme'
import { updateTheme } from '../../lib/themeUpdater'
import { Priority, useRegisterActions } from 'kbar'
import { ThemeId, theme } from '../tokens/stitches.config'
import { NavigationMenu } from './navMenu/NavigationMenu'
import { DEFAULT_HEADER_HEIGHT } from './homeFeed/HeaderSpacer'
import { Button } from '../elements/Button'
import { List } from 'phosphor-react'
import { usePersistedState } from '../../lib/hooks/usePersistedState'
export type NavigationSection =
| 'justread'
| 'home'
| 'library'
| 'subscriptions'
| 'highlights'
type NavigationLayoutProps = {
children: ReactNode
section: NavigationSection
pageMetaDataProps?: PageMetaDataProps
}
export function NavigationLayout(props: NavigationLayoutProps): JSX.Element {
useApplyLocalTheme()
const { viewerData } = useGetViewerQuery()
const router = useRouter()
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false)
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] =
useState(false)
const [showLeftMenu, setShowLeftMenu] = usePersistedState<boolean>({
key: 'nav-show-menu',
isSessionStorage: false,
initialValue: true,
})
useKeyboardShortcuts(navigationCommands(router))
useKeyboardShortcuts(
primaryCommands((action) => {
switch (action) {
case 'toggleShortcutHelpModalDisplay':
setShowKeyboardCommandsModal(true)
break
}
})
)
useRegisterActions(
[
{
id: 'home',
section: 'Navigation',
name: 'Go to Home (Library) ',
shortcut: ['g h'],
keywords: 'go home',
perform: () => router?.push('/home'),
},
{
id: 'lightTheme',
section: 'Preferences',
name: 'Change theme (light) ',
shortcut: ['v', 'l'],
keywords: 'light theme',
priority: Priority.LOW,
perform: () => updateTheme(ThemeId.Light),
},
{
id: 'darkTheme',
section: 'Preferences',
name: 'Change theme (dark) ',
shortcut: ['v', 'd'],
keywords: 'dark theme',
priority: Priority.LOW,
perform: () => updateTheme(ThemeId.Dark),
},
],
[router]
)
// Attempt to identify the user if they are logged in.
useEffect(() => {
setupAnalytics(viewerData?.me)
}, [viewerData?.me])
const showLogout = useCallback(() => {
setShowLogoutConfirmation(true)
}, [setShowLogoutConfirmation])
useEffect(() => {
document.addEventListener('logout', showLogout)
return () => {
document.removeEventListener('logout', showLogout)
}
}, [showLogout])
return (
<>
{props.pageMetaDataProps ? (
<PageMetaData {...props.pageMetaDataProps} />
) : null}
<Box
css={{
height: '100%',
width: '100vw',
bg: '$thBackground2',
}}
>
<Header
toggleMenu={() => {
setShowLeftMenu(!showLeftMenu)
}}
/>
{showLeftMenu && (
<NavigationMenu
section={props.section}
// eslint-disable-next-line @typescript-eslint/no-empty-function
setShowAddLinkModal={() => {}}
searchTerm={''}
// eslint-disable-next-line @typescript-eslint/no-empty-function
applySearchQuery={(searchQuery: string) => {}}
showFilterMenu={showLeftMenu}
setShowFilterMenu={(show) => {
setShowLeftMenu(show)
}}
/>
)}
{props.children}
{showLogoutConfirmation ? (
<ConfirmationModal
message={'Are you sure you want to log out?'}
onAccept={logout}
onOpenChange={() => setShowLogoutConfirmation(false)}
/>
) : null}
{showKeyboardCommandsModal ? (
<KeyboardShortcutListModal
onOpenChange={() => setShowKeyboardCommandsModal(false)}
/>
) : null}
</Box>
</>
)
}
type HeaderProps = {
toggleMenu: () => void
}
const Header = (props: HeaderProps): JSX.Element => {
const small = false
return (
<VStack
alignment="start"
distribution="start"
css={{
zIndex: 5,
position: 'fixed',
left: '15px',
top: '15px',
height: small ? '60px' : DEFAULT_HEADER_HEIGHT,
transition: 'height 0.5s',
'@lgDown': { px: '20px' },
'@mdDown': {
px: '10px',
left: '0px',
right: '0',
},
}}
>
<VStack alignment="center" distribution="center">
<Button
style="plainIcon"
onClick={(event) => {
props.toggleMenu()
event.preventDefault()
}}
>
<List size="25" color={theme.colors.readerTextSubtle.toString()} />
</Button>
</VStack>
</VStack>
)
}

View file

@ -32,10 +32,13 @@ import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { useRouter } from 'next/router'
import { DiscoverIcon } from '../../elements/icons/DiscoverIcon'
import { escapeQuotes } from '../../../utils/helper'
import { NavigationSection } from '../NavigationLayout'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
type LibraryFilterMenuProps = {
section: NavigationSection
setShowAddLinkModal: (show: boolean) => void
searchTerm: string | undefined
@ -206,33 +209,31 @@ const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => {
<NavButton
{...props}
text="Home"
filterTerm="in:library OR in:following use:folders"
section="justread"
isSelected={props.section == 'home'}
icon={<HomeIcon color={theme.colors.thHomeIcon.toString()} />}
/>
<NavButton
{...props}
text="Following"
filterTerm="in:following use:folders"
text="Subscriptions"
section="subscriptions"
isSelected={props.section == 'subscriptions'}
icon={<FollowingIcon color="#F59932" />}
/>
<NavButton
{...props}
text="Library"
filterTerm="in:library use:folders"
section="library"
isSelected={props.section == 'library'}
icon={<LibraryIcon color={theme.colors.ctaBlue.toString()} />}
/>
<NavButton
{...props}
text="Highlights"
filterTerm="in:all has:highlights mode:highlights"
section="highlights"
isSelected={props.section == 'highlights'}
icon={<HighlightsIcon color={theme.colors.highlight.toString()} />}
/>
<NavRedirectButton
{...props}
text="Discover"
redirectLocation={'/discover'}
icon={<DiscoverIcon color={theme.colors.discover.toString()} />}
/>
</VStack>
)
}
@ -731,11 +732,8 @@ type NavButtonProps = {
text: string
icon: ReactNode
filterTerm: string
searchTerm: string | undefined
applySearchQuery: (searchTerm: string) => void
setShowFilterMenu: (show: boolean) => void
isSelected: boolean
section: NavigationSection
}
type NavButtonRedirectProps = {
@ -803,15 +801,7 @@ function NavRedirectButton(props: NavButtonRedirectProps): JSX.Element {
}
function NavButton(props: NavButtonProps): JSX.Element {
const isInboxFilter = (filter: string) => {
return filter === '' || filter === 'in:inbox'
}
const selected = useMemo(() => {
if (isInboxFilter(props.filterTerm) && !props.searchTerm) {
return true
}
return props.searchTerm === props.filterTerm
}, [props.searchTerm, props.filterTerm])
const router = useRouter()
return (
<HStack
@ -826,11 +816,13 @@ function NavButton(props: NavButtonProps): JSX.Element {
maxWidth: '100%',
height: '34px',
backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
backgroundColor: props.isSelected
? '$thLibrarySelectionColor'
: 'unset',
fontSize: '15px',
fontWeight: 'regular',
fontFamily: '$display',
color: selected
color: props.isSelected
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
@ -840,21 +832,19 @@ function NavButton(props: NavButtonProps): JSX.Element {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
'&:hover': {
backgroundColor: selected
backgroundColor: props.isSelected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
'&:active': {
backgroundColor: selected
backgroundColor: props.isSelected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
}}
title={props.text}
onClick={(e) => {
props.applySearchQuery(props.filterTerm)
props.setShowFilterMenu(false)
e.preventDefault()
router.push(`/` + props.section)
}}
>
{props.icon}

View file

@ -0,0 +1,29 @@
import { NavigationLayout } from '../../components/templates/NavigationLayout'
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
import { HomeFeedContainer } from '../../components/templates/homeFeed/HomeFeedContainer'
import { VStack } from '../../components/elements/LayoutPrimitives'
export default function Highlights(): JSX.Element {
return (
<NavigationLayout
section="highlights"
pageMetaDataProps={{
title: 'Highlights',
path: '/highlights',
}}
>
<VStack
alignment="start"
distribution="center"
css={{
px: '70px',
backgroundColor: '$thLibraryBackground',
'@lgDown': { px: '20px' },
'@mdDown': { px: '10px' },
}}
>
<div>Highlights will go here</div>
</VStack>
</NavigationLayout>
)
}

View file

@ -30,97 +30,77 @@ import {
SpanBox,
VStack,
} from './../../components/elements/LayoutPrimitives'
import { List, ThumbsDown, ThumbsUp } from 'phosphor-react'
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
import { Toaster } from 'react-hot-toast'
import { DEFAULT_HEADER_HEIGHT } from '../../components/templates/homeFeed/HeaderSpacer'
import { NavigationMenu } from '../../components/templates/navMenu/NavigationMenu'
import { NavigationLayout } from '../../components/templates/NavigationLayout'
export default function Home(): JSX.Element {
const [showLeftMenu, setShowLeftMenu] = useState(false)
const homeData = useGetHomeItems()
useApplyLocalTheme()
return (
<VStack
distribution="start"
alignment="center"
css={{
width: '100%',
bg: '$readerBg',
pt: '45px',
minHeight: '100vh',
}}
>
<Toaster />
<Header
toggleMenu={() => {
setShowLeftMenu(!showLeftMenu)
}}
/>
{showLeftMenu && (
<NavigationMenu
// eslint-disable-next-line @typescript-eslint/no-empty-function
setShowAddLinkModal={() => {}}
searchTerm={''}
// eslint-disable-next-line @typescript-eslint/no-empty-function
applySearchQuery={(searchQuery: string) => {}}
showFilterMenu={showLeftMenu}
setShowFilterMenu={(show) => {
setShowLeftMenu(show)
}}
/>
)}
<NavigationLayout section="justread">
<VStack
distribution="start"
alignment="center"
css={{
width: '646px',
gap: '40px',
width: '100%',
bg: '$readerBg',
pt: '45px',
minHeight: '100vh',
'@mdDown': {
width: '100%',
},
}}
>
{homeData.sections?.map((homeSection, idx) => {
if (homeSection.items.length < 1) {
return <></>
}
switch (homeSection.layout) {
case 'just_added':
return (
<JustAddedHomeSection
key={`section-${idx}`}
homeSection={homeSection}
/>
)
case 'top_picks':
return (
<TopPicksHomeSection
key={`section-${idx}`}
homeSection={homeSection}
/>
)
case 'quick_links':
return (
<QuickLinksHomeSection
key={`section-${idx}`}
homeSection={homeSection}
/>
)
case 'hidden':
return (
<HiddenHomeSection
key={`section-${idx}`}
homeSection={homeSection}
/>
)
default:
<Toaster />
<VStack
distribution="start"
css={{
width: '646px',
gap: '40px',
minHeight: '100vh',
'@mdDown': {
width: '100%',
},
}}
>
{homeData.sections?.map((homeSection, idx) => {
if (homeSection.items.length < 1) {
return <></>
}
})}
}
switch (homeSection.layout) {
case 'just_added':
return (
<JustAddedHomeSection
key={`section-${idx}`}
homeSection={homeSection}
/>
)
case 'top_picks':
return (
<TopPicksHomeSection
key={`section-${idx}`}
homeSection={homeSection}
/>
)
case 'quick_links':
return (
<QuickLinksHomeSection
key={`section-${idx}`}
homeSection={homeSection}
/>
)
case 'hidden':
return (
<HiddenHomeSection
key={`section-${idx}`}
homeSection={homeSection}
/>
)
default:
return <></>
}
})}
</VStack>
</VStack>
</VStack>
</NavigationLayout>
)
}
@ -753,44 +733,3 @@ const SubscriptionSourceHoverContent = (
// </HStack>
// )
// }
type HeaderProps = {
toggleMenu: () => void
}
const Header = (props: HeaderProps): JSX.Element => {
const small = false
return (
<VStack
alignment="start"
distribution="start"
css={{
zIndex: 5,
position: 'fixed',
left: '15px',
top: '15px',
height: small ? '60px' : DEFAULT_HEADER_HEIGHT,
transition: 'height 0.5s',
'@lgDown': { px: '20px' },
'@mdDown': {
px: '10px',
left: '0px',
right: '0',
},
}}
>
<VStack alignment="center" distribution="center">
<Button
style="plainIcon"
onClick={(event) => {
props.toggleMenu()
event.preventDefault()
}}
>
<List size="25" color={theme.colors.readerTextSubtle.toString()} />
</Button>
</VStack>
</VStack>
)
}

View file

@ -0,0 +1,33 @@
import { NavigationLayout } from '../../components/templates/NavigationLayout'
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
import { HomeFeedContainer } from '../../components/templates/homeFeed/HomeFeedContainer'
import { VStack } from '../../components/elements/LayoutPrimitives'
export default function Home(): JSX.Element {
return <LoadedContent />
}
function LoadedContent(): JSX.Element {
return (
<NavigationLayout
section="library"
pageMetaDataProps={{
title: 'Library',
path: '/library',
}}
>
<VStack
alignment="start"
distribution="center"
css={{
px: '70px',
backgroundColor: '$thLibraryBackground',
'@lgDown': { px: '20px' },
'@mdDown': { px: '10px' },
}}
>
<HomeFeedContainer />
</VStack>
</NavigationLayout>
)
}

View file

@ -0,0 +1,29 @@
import { NavigationLayout } from '../../components/templates/NavigationLayout'
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
import { HomeFeedContainer } from '../../components/templates/homeFeed/HomeFeedContainer'
import { VStack } from '../../components/elements/LayoutPrimitives'
export default function Subscriptions(): JSX.Element {
return (
<NavigationLayout
section="subscriptions"
pageMetaDataProps={{
title: 'Subscriptions',
path: '/subscriptions',
}}
>
<VStack
alignment="start"
distribution="center"
css={{
px: '70px',
backgroundColor: '$thLibraryBackground',
'@lgDown': { px: '20px' },
'@mdDown': { px: '10px' },
}}
>
<div>Subscriptions will go here</div>
</VStack>
</NavigationLayout>
)
}