omnivore/packages/web/components/templates/homeFeed/LibraryHeader.tsx
2023-02-25 08:51:25 +08:00

374 lines
9.7 KiB
TypeScript

import {
InputHTMLAttributes,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import { StyledText } from '../../elements/StyledText'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { SearchIcon } from '../../elements/images/SearchIcon'
import { theme, ThemeId } from '../../tokens/stitches.config'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { FormInput } from '../../elements/FormElements'
import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { Button, IconButton } from '../../elements/Button'
import { MagnifyingGlass, Textbox, X } from 'phosphor-react'
import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo'
import { OmnivoreFullLogo } from '../../elements/images/OmnivoreFullLogo'
import { AvatarDropdown } from '../../elements/AvatarDropdown'
import { ListSelectorIcon } from '../../elements/images/ListSelectorIcon'
import { GridSelectorIcon } from '../../elements/images/GridSelectorIcon'
import { LayoutType } from './HomeFeedContainer'
import { DropdownMenu, HeaderDropdownAction } from '../../patterns/DropdownMenu'
import { updateTheme } from '../../../lib/themeUpdater'
import { useRouter } from 'next/router'
type LibraryHeaderProps = {
layout: LayoutType
updateLayout: (layout: LayoutType) => void
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
}
const FOCUSED_BOXSHADOW = '0px 0px 2px 2px rgba(255, 234, 159, 0.56)'
const HEADER_HEIGHT = '105px'
const MOBILE_HEIGHT = '48px'
export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
return (
<>
<VStack
alignment="center"
distribution="start"
css={{
top: '0',
left: '0',
zIndex: 5,
position: 'fixed',
width: '100%',
height: HEADER_HEIGHT,
bg: 'white',
pt: '35px',
borderBottom: '1px solid #E1E1E1',
'@mdDown': {
height: MOBILE_HEIGHT,
pt: '0px',
},
}}
>
<HStack
alignment="center"
distribution="start"
css={{
width: '100%',
height: '100%',
}}
>
<LogoBox />
<SearchBox {...props} />
<ControlButtonBox
layout={props.layout}
updateLayout={props.updateLayout}
/>
</HStack>
</VStack>
{/* This spacer is put in to push library content down
below the fixed header height. */}
<Box
css={{
height: HEADER_HEIGHT,
bg: '$grayBase',
'@mdDown': {
height: MOBILE_HEIGHT,
},
}}
></Box>
</>
)
}
type SearchBoxProps = {
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
}
function SearchBox(props: SearchBoxProps): JSX.Element {
const inputRef = useRef<HTMLInputElement | null>(null)
const [focused, setFocused] = useState(false)
const [searchTerm, setSearchTerm] = useState(props.searchTerm ?? '')
useKeyboardShortcuts(
searchBarCommands((action) => {
if (action === 'focusSearchBar' && inputRef.current) {
inputRef.current.select()
}
})
)
return (
<Box
css={{
height: '38px',
width: '100%',
maxWidth: '521px',
mr: '15px',
bg: '#F3F3F3',
borderRadius: '6px',
'@mdDown': {
display: 'none',
},
boxShadow: focused ? FOCUSED_BOXSHADOW : 'unset',
}}
>
<HStack
alignment="center"
distribution="start"
css={{ width: '100%', height: '100%' }}
>
<HStack
alignment="center"
distribution="start"
css={{ height: '100%', px: '15px' }}
onClick={(e) => {
inputRef.current?.focus()
e.preventDefault()
}}
>
<MagnifyingGlass
size={20}
color={theme.colors.graySolid.toString()}
/>
</HStack>
<form
onSubmit={(event) => {
event.preventDefault()
props.applySearchQuery(searchTerm || '')
inputRef.current?.blur()
}}
style={{ width: '100%' }}
>
<FormInput
ref={inputRef}
type="text"
value={searchTerm}
placeholder="Search"
onFocus={(event) => {
event.target.select()
setFocused(true)
}}
onBlur={() => {
setFocused(false)
}}
onChange={(event) => {
setSearchTerm(event.target.value)
}}
/>
</form>
{props.searchTerm ? (
<Button
style="plainIcon"
onClick={(event) => {
event.preventDefault()
setSearchTerm('')
props.applySearchQuery('')
inputRef.current?.blur()
}}
css={{
display: 'flex',
flexDirection: 'row',
mr: '8px',
height: '100%',
alignItems: 'center',
}}
>
<X
width={16}
height={16}
color={theme.colors.grayTextContrast.toString()}
/>
</Button>
) : (
<Box
css={{
py: '15px',
marginLeft: 'auto',
}}
>
<IconButton
css={{
mr: '5px',
width: '28px',
height: '28px',
color: '#898989',
}}
onClick={() =>
requestAnimationFrame(() => inputRef?.current?.focus())
}
// we can make it unreachable via keyboard as we have the same message for the SR label
tabIndex={-1}
>
<kbd aria-hidden>/</kbd>
</IconButton>
</Box>
)}
</HStack>
</Box>
)
}
// Displays the full logo on larger screens, small logo on mobile
function LogoBox(): JSX.Element {
return (
<>
<SpanBox
css={{
ml: '25px',
height: '24px',
width: '232px',
minWidth: '232px',
'@mdDown': {
display: 'none',
},
}}
>
<OmnivoreFullLogo showTitle={true} />
</SpanBox>
<SpanBox
css={{
ml: '20px',
mr: '20px',
'@md': {
display: 'none',
},
}}
>
<OmnivoreNameLogo />
</SpanBox>
</>
)
}
type ControlButtonBoxProps = {
layout: LayoutType
updateLayout: (layout: LayoutType) => void
}
function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element {
const router = useRouter()
const headerDropdownActionHandler = useCallback(
(action: HeaderDropdownAction) => {
switch (action) {
case 'apply-darker-theme':
updateTheme(ThemeId.Darker)
break
case 'apply-dark-theme':
updateTheme(ThemeId.Dark)
break
case 'apply-lighter-theme':
updateTheme(ThemeId.Lighter)
break
case 'apply-light-theme':
updateTheme(ThemeId.Light)
break
case 'navigate-to-install':
router.push('/settings/installation')
break
case 'navigate-to-emails':
router.push('/settings/emails')
break
case 'navigate-to-labels':
router.push('/settings/labels')
break
case 'navigate-to-subscriptions':
router.push('/settings/subscriptions')
break
case 'navigate-to-api':
router.push('/settings/api')
break
case 'navigate-to-integrations':
router.push('/settings/integrations')
break
case 'logout':
// props.setShowLogoutConfirmation(true)
break
default:
break
}
},
[updateTheme, router]
)
return (
<>
<HStack
alignment="center"
distribution="end"
css={{
marginLeft: 'auto',
marginRight: '45px',
width: '100px',
height: '100%',
gap: '20px',
minWidth: '121px',
'@mdDown': {
display: 'none',
},
}}
>
<Button
style="plainIcon"
css={{ display: 'flex' }}
onClick={(e) => {
props.updateLayout('LIST_LAYOUT')
e.preventDefault()
}}
>
<ListSelectorIcon
color={props.layout == 'GRID_LAYOUT' ? '#6A6968' : '#FFEA9F'}
/>
</Button>
<Button
style="plainIcon"
css={{ display: 'flex' }}
onClick={(e) => {
props.updateLayout('GRID_LAYOUT')
e.preventDefault()
}}
>
<GridSelectorIcon
color={props.layout == 'LIST_LAYOUT' ? '#6A6968' : '#FFEA9F'}
/>
</Button>
<DropdownMenu
username={'props.username'}
triggerElement={<AvatarDropdown userInitials="JH" />}
actionHandler={headerDropdownActionHandler}
/>
</HStack>
<HStack
alignment="center"
distribution="end"
css={{
marginLeft: 'auto',
marginRight: '20px',
width: '100px',
height: '100%',
gap: '20px',
'@md': {
display: 'none',
},
}}
>
<AvatarDropdown userInitials="JH" />
</HStack>
</>
)
}