337 lines
9.0 KiB
JavaScript
337 lines
9.0 KiB
JavaScript
import { Capacitor } from '@capacitor/core'
|
|
import {
|
|
Archive,
|
|
ArrowBack,
|
|
FilterAlt,
|
|
FolderOpen,
|
|
History,
|
|
Inbox,
|
|
ListAlt,
|
|
Logout,
|
|
MenuRounded,
|
|
SearchRounded,
|
|
SettingsOutlined,
|
|
Toll,
|
|
Widgets,
|
|
} from '@mui/icons-material'
|
|
import {
|
|
Box,
|
|
Drawer,
|
|
IconButton,
|
|
List,
|
|
ListItemButton,
|
|
ListItemContent,
|
|
ListItemDecorator,
|
|
Typography,
|
|
} from '@mui/joy'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
|
|
|
import { version } from '../../../package.json'
|
|
import UserProfileAvatar from '../../components/UserProfileAvatar'
|
|
import Z_INDEX from '../../constants/zIndex'
|
|
import { useResource } from '../../queries/ResourceQueries'
|
|
import { useGlobalSearch } from '../../search/GlobalSearchContext'
|
|
import { apiClient } from '../../utils/ApiClient'
|
|
import NavBarLink from './NavBarLink'
|
|
import SyncStatusIndicator from './SyncStatusIndicator'
|
|
|
|
const publicPages = ['/landing', '/privacy', '/terms']
|
|
const NavBar = () => {
|
|
const { t } = useTranslation('common')
|
|
const { data: resource } = useResource()
|
|
const { openSearch } = useGlobalSearch()
|
|
|
|
const navigate = useNavigate()
|
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
|
|
const links = [
|
|
{
|
|
label: t('navigation.search'),
|
|
icon: <SearchRounded />,
|
|
onClick: () => openSearch(),
|
|
},
|
|
{
|
|
to: '/chores',
|
|
label: t('navigation.allTasks'),
|
|
icon: <Inbox />,
|
|
},
|
|
{
|
|
to: '/archived',
|
|
label: t('navigation.archived'),
|
|
icon: <Archive />,
|
|
},
|
|
{
|
|
to: '/things',
|
|
label: t('navigation.things'),
|
|
icon: <Widgets />,
|
|
},
|
|
{
|
|
to: 'labels',
|
|
label: t('navigation.labels'),
|
|
icon: <ListAlt />,
|
|
},
|
|
{
|
|
to: 'projects',
|
|
label: t('navigation.projects'),
|
|
icon: <FolderOpen />,
|
|
},
|
|
{
|
|
to: 'filters',
|
|
label: t('navigation.filters'),
|
|
icon: <FilterAlt />,
|
|
},
|
|
{
|
|
to: 'activities',
|
|
label: t('navigation.activities'),
|
|
icon: <History />,
|
|
},
|
|
{
|
|
to: 'points',
|
|
label: t('navigation.points'),
|
|
icon: <Toll />,
|
|
},
|
|
{
|
|
to: '/settings',
|
|
label: t('navigation.settings'),
|
|
icon: <SettingsOutlined />,
|
|
},
|
|
]
|
|
const [openDrawer, closeDrawer] = [
|
|
() => setDrawerOpen(true),
|
|
() => setDrawerOpen(false),
|
|
]
|
|
const location = useLocation()
|
|
const [searchParams] = useSearchParams()
|
|
const getMenuIcon = () => {
|
|
const menuRounded = (
|
|
<IconButton size='md' variant='plain' onClick={() => setDrawerOpen(true)}>
|
|
<MenuRounded />
|
|
</IconButton>
|
|
)
|
|
if (location.pathname === '/search') {
|
|
return (
|
|
<IconButton
|
|
size='md'
|
|
variant='plain'
|
|
onClick={() => {
|
|
if (window.history.state?.idx > 0) navigate(-1)
|
|
else navigate('/chores', { replace: true })
|
|
}}
|
|
aria-label='Back from search'
|
|
title={t('back')}
|
|
>
|
|
<ArrowBack className='rtl-flip' />
|
|
</IconButton>
|
|
)
|
|
}
|
|
if (!Capacitor.isNativePlatform()) {
|
|
return menuRounded
|
|
}
|
|
if (
|
|
['/chores', '/'].includes(location.pathname) &&
|
|
!searchParams.get('filterId')
|
|
) {
|
|
return menuRounded
|
|
}
|
|
return (
|
|
<IconButton
|
|
size='md'
|
|
variant='plain'
|
|
onClick={() => {
|
|
if (location.pathname === '/chores') {
|
|
// Navigate back to calendar view
|
|
navigate('/')
|
|
} else {
|
|
// Default back navigation
|
|
navigate(-1)
|
|
}
|
|
}}
|
|
title={
|
|
searchParams.get('from') === 'calendar'
|
|
? t('backToCalendar')
|
|
: t('back')
|
|
}
|
|
>
|
|
<ArrowBack className='rtl-flip' />
|
|
</IconButton>
|
|
)
|
|
}
|
|
|
|
if (
|
|
[
|
|
'/signup',
|
|
'/login',
|
|
'/auth/oauth2',
|
|
'/forgot-password',
|
|
'/password/update',
|
|
'/login/settings',
|
|
'/welcome',
|
|
'/onboarding',
|
|
'/get-started',
|
|
'/ready',
|
|
// Reached from an invite link, often signed out: it owns its own shell
|
|
// and must not mount the avatar's authenticated queries.
|
|
'/circle/join',
|
|
].includes(location.pathname)
|
|
) {
|
|
return (
|
|
// no navbar but show the safe area padding
|
|
<div
|
|
style={{
|
|
paddingTop: `calc(var(--safe-area-inset-top, 0px))`,
|
|
top: 0,
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
// if url has /landing then remove the navbar:
|
|
if (publicPages.includes(location.pathname)) {
|
|
return null
|
|
}
|
|
if (
|
|
window.location.hostname === 'www.donetick.com' ||
|
|
window.location.hostname === 'donetick.com'
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<nav
|
|
className='flex gap-2 p-3'
|
|
style={{
|
|
paddingTop:
|
|
Capacitor.getPlatform() === 'android'
|
|
? `calc(var(--safe-area-inset-top, 0px))`
|
|
: '',
|
|
position: 'sticky',
|
|
zIndex: Z_INDEX.NAVBAR,
|
|
top: 0,
|
|
minHeight: '35px',
|
|
backgroundColor: 'var(--joy-palette-background-body)',
|
|
}}
|
|
>
|
|
{getMenuIcon()}
|
|
<Box className='flex-1' />
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<SyncStatusIndicator />
|
|
<UserProfileAvatar />
|
|
{/* <ThemeToggleButton /> */}
|
|
</Box>
|
|
<Drawer
|
|
open={drawerOpen}
|
|
onClose={closeDrawer}
|
|
// Always 'left'. Joy bakes the anchor into emotion CSS (`left: 0` plus a
|
|
// translateX for the slide), so stylis-plugin-rtl already mirrors it to
|
|
// the right edge under RTL. Branching on isRTL here would flip it twice
|
|
// and land the drawer back on the left, half off-screen.
|
|
anchor='left'
|
|
size='sm'
|
|
onClick={closeDrawer}
|
|
sx={{
|
|
'& .MuiDrawer-content': {
|
|
position: 'fixed',
|
|
// pt: 'calc(var(--safe-area-inset-top, 0px))',
|
|
// Physical on purpose, so it is mirrored in step with the anchor.
|
|
left: 0,
|
|
// pb: 'calc(var(--safe-area-inset-bottom, 0px))',
|
|
// height:
|
|
// 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',
|
|
overflow: 'auto',
|
|
zIndex: Z_INDEX.DRAWER,
|
|
},
|
|
}}
|
|
>
|
|
{/* Safe-area padding comes from the --safe-area-inset-* variables that
|
|
Capacitor's SystemBars keeps in sync with the live window insets.
|
|
Top inset is left to the inner List so it isn't applied twice. */}
|
|
<div className='drawer-content safe-area-x safe-area-bottom'>
|
|
{/* <div className='align-center flex px-5 pt-4'>
|
|
<ModalClose size='sm' sx={{ top: 'unset', right: 20 }} />
|
|
</div> */}
|
|
<List
|
|
// sx={{ p: 2, height: 'min-content' }}
|
|
|
|
size='md'
|
|
onClick={openDrawer}
|
|
sx={{
|
|
borderRadius: 4,
|
|
width: '100%',
|
|
padding: 1,
|
|
paddingTop:
|
|
Capacitor.getPlatform() === 'android'
|
|
? `calc(var(--safe-area-inset-top, 0px))`
|
|
: '',
|
|
}}
|
|
>
|
|
{links.map((link, index) => (
|
|
<NavBarLink key={index} link={link} />
|
|
))}
|
|
</List>
|
|
</div>
|
|
<div>
|
|
<List
|
|
sx={{
|
|
p: 2,
|
|
height: 'min-content',
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
borderRadius: 4,
|
|
width: '100%',
|
|
padding: 2,
|
|
}}
|
|
size='md'
|
|
onClick={openDrawer}
|
|
>
|
|
{/* Add List item to invite the user to upgrade to Plus: */}
|
|
{/* <ListItemButton
|
|
onClick={() => navigate('/settings#subscription')}
|
|
sx={{
|
|
py: 1.2,
|
|
}}
|
|
>
|
|
<ListItemDecorator>
|
|
<SwitchAccessShortcutAdd />
|
|
</ListItemDecorator>
|
|
<ListItemContent>Upgrade to Plus</ListItemContent>
|
|
</ListItemButton> */}
|
|
<ListItemButton
|
|
onClick={() => {
|
|
apiClient.handleLogout()
|
|
}}
|
|
sx={{
|
|
py: 1.2,
|
|
}}
|
|
>
|
|
<ListItemDecorator>
|
|
<Logout />
|
|
</ListItemDecorator>
|
|
<ListItemContent>{t('logout')}</ListItemContent>
|
|
</ListItemButton>
|
|
<Typography
|
|
onClick={
|
|
// force service worker to update:
|
|
() => window.location.reload(true)
|
|
}
|
|
level='body-xs'
|
|
sx={{
|
|
// p: 2,
|
|
p: 1,
|
|
color: 'text.tertiary',
|
|
textAlign: 'center',
|
|
mb: 'calc(var(--safe-area-inset-bottom, 0px) )',
|
|
// mb: -2,
|
|
}}
|
|
>
|
|
V{version} (API: {resource?.api_version || 'unavailable'})
|
|
</Typography>
|
|
</List>
|
|
</div>
|
|
</Drawer>
|
|
</nav>
|
|
)
|
|
}
|
|
|
|
export default NavBar
|