mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Setup new left navigation system
This commit is contained in:
parent
648cbe18cf
commit
2123b248c9
6 changed files with 368 additions and 149 deletions
199
packages/web/components/templates/NavigationLayout.tsx
Normal file
199
packages/web/components/templates/NavigationLayout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
29
packages/web/pages/highlights/index.tsx
Normal file
29
packages/web/pages/highlights/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
33
packages/web/pages/library/index.tsx
Normal file
33
packages/web/pages/library/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
packages/web/pages/subscriptions/index.tsx
Normal file
29
packages/web/pages/subscriptions/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue