Merge pull request #3078 from omnivore-app/feat/ios-header-pins

Header pins on the web
This commit is contained in:
Jackson Harper
2023-11-08 13:25:32 +08:00
committed by GitHub
15 changed files with 629 additions and 81 deletions

View File

@ -174,14 +174,36 @@ export const Button = styled('button', {
},
},
ctaPill: {
borderRadius: '$3',
px: '$3',
py: '$2',
border: '1px solid $grayBorder',
bg: '$grayBgActive',
cursor: 'pointer',
borderRadius: '15px',
px: '12px',
py: '5px',
font: '$inter',
fontSize: '12px',
fontWeight: '700',
whiteSpace: 'nowrap',
color: '$thLibraryMenuPrimary',
border: '1px solid $thLeftMenuBackground',
backgroundColor: '$thLeftMenuBackground',
'&:hover': {
bg: '$grayBgHover',
border: '1px solid $grayBorderHover',
bg: '$thBackgroundActive',
border: '1px solid $thBackgroundActive',
},
},
ctaPillUnselected: {
cursor: 'pointer',
borderRadius: '15px',
px: '12px',
py: '5px',
font: '$inter',
fontSize: '12px',
fontWeight: 'medium',
whiteSpace: 'nowrap',
border: '1px solid $thBackground4',
backgroundColor: '$thBackground4',
'&:hover': {
bg: '$thBackgroundActive',
border: '1px solid $thBackgroundActive',
},
},
link: {
@ -193,24 +215,6 @@ export const Button = styled('button', {
color: '$thLibraryMenuUnselected',
cursor: 'pointer',
},
circularIcon: {
mx: '$1',
display: 'flex',
alignItems: 'center',
fontWeight: 500,
height: 44,
width: 44,
borderRadius: '50%',
justifyContent: 'center',
textAlign: 'center',
background: '$grayBase',
cursor: 'pointer',
border: 'none',
opacity: 0.9,
'&:hover': {
opacity: 1,
},
},
squareIcon: {
mx: '$1',
display: 'flex',
@ -246,7 +250,6 @@ export const Button = styled('button', {
'&:hover': {
opacity: 0.5,
},
},
articleActionIcon: {
bg: 'transparent',

View File

@ -2,7 +2,7 @@ import { Box, HStack } from '../elements/LayoutPrimitives'
import { OmnivoreNameLogo } from '../elements/images/OmnivoreNameLogo'
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
import { PrimaryDropdown } from '../templates/PrimaryDropdown'
import { HEADER_HEIGHT } from '../templates/homeFeed/HeaderSpacer'
import { DEFAULT_HEADER_HEIGHT } from '../templates/homeFeed/HeaderSpacer'
import { LogoBox } from '../elements/LogoBox'
type HeaderProps = {
@ -23,7 +23,7 @@ export function SettingsHeader(props: HeaderProps): JSX.Element {
position: 'fixed',
width: '100%',
pr: '25px',
height: HEADER_HEIGHT,
height: DEFAULT_HEADER_HEIGHT,
'@mdDown': {
pr: '15px',
},

View File

@ -206,7 +206,7 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
)
}
const StyledToggleButton = styled('button', {
export const StyledToggleButton = styled('button', {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',

View File

@ -5,13 +5,11 @@ import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortc
import { useKeyboardShortcuts } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { useRouter } from 'next/router'
import { applyStoredTheme } from '../../lib/themeUpdater'
import { logoutMutation } from '../../lib/networking/mutations/logoutMutation'
import { useCallback, useEffect, useState } from 'react'
import { ConfirmationModal } from '../patterns/ConfirmationModal'
import { KeyboardShortcutListModal } from './KeyboardShortcutListModal'
import { PageMetaData } from '../patterns/PageMetaData'
import { HEADER_HEIGHT } from './homeFeed/HeaderSpacer'
import { deinitAnalytics } from '../../lib/analytics'
import { DEFAULT_HEADER_HEIGHT } from './homeFeed/HeaderSpacer'
import { logout } from '../../lib/logout'
import { SettingsMenu } from './SettingsMenu'
@ -53,7 +51,7 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
<VStack css={{ width: '100%', height: '100%' }}>
<Box
css={{
height: HEADER_HEIGHT,
height: DEFAULT_HEADER_HEIGHT,
}}
></Box>
<HStack css={{ width: '100%', height: '100%' }} distribution="start">

View File

@ -86,6 +86,8 @@ export function SettingsMenu(): JSX.Element {
{ name: 'Feeds', destination: '/settings/feeds' },
{ name: 'Subscriptions', destination: '/settings/subscriptions' },
{ name: 'Labels', destination: '/settings/labels' },
{ name: 'Saved Searches', destination: '/settings/saved-searches' },
{ name: 'Pinned Searches', destination: '/settings/pinned-searches' },
]
const section2 = [
@ -249,5 +251,5 @@ function SettingsButton(props: SettingsButtonProps): JSX.Element {
{props.name}
</SpanBox>
</Link>
);
)
}

View File

@ -16,7 +16,7 @@ import { pspdfKitKey } from '../../../lib/appConfig'
import { NotebookModal } from './NotebookModal'
import { HighlightNoteModal } from './HighlightNoteModal'
import { showErrorToast } from '../../../lib/toastHelpers'
import { HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { DEFAULT_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import Epub, { EpubCFI } from 'epubjs'
import { Rendition, Contents } from 'epubjs/types'
@ -294,7 +294,7 @@ export default function EpubContainer(props: EpubContainerProps): JSX.Element {
paddingBottom: '0px',
},
width: '100%',
height: `calc(100vh - ${HEADER_HEIGHT})`,
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
}}
>
<Box

View File

@ -14,7 +14,7 @@ import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeH
import { pspdfKitKey } from '../../../lib/appConfig'
import { HighlightNoteModal } from './HighlightNoteModal'
import { showErrorToast } from '../../../lib/toastHelpers'
import { HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { DEFAULT_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import SlidingPane from 'react-sliding-pane'
import 'react-sliding-pane/dist/react-sliding-pane.css'
@ -569,7 +569,7 @@ export default function PdfArticleContainer(
id="article-wrapper"
css={{
width: '100%',
height: `calc(100vh - ${HEADER_HEIGHT})`,
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
}}
>
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />

View File

@ -2,8 +2,10 @@ import { Book } from 'phosphor-react'
import { VStack } from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { theme } from '../../tokens/stitches.config'
import { useGetHeaderHeight } from './HeaderSpacer'
export function EmptyHighlights(): JSX.Element {
const headerHeight = useGetHeaderHeight()
return (
<VStack
alignment="center"
@ -11,7 +13,7 @@ export function EmptyHighlights(): JSX.Element {
css={{
color: '$grayTextContrast',
textAlign: 'center',
marginTop: '70px',
marginTop: headerHeight,
}}
>
<Book size={44} color={theme.colors.grayTextContrast.toString()} />

View File

@ -1,12 +1,33 @@
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { Box } from '../../elements/LayoutPrimitives'
export const HEADER_HEIGHT = '70px'
export const DEFAULT_HEADER_HEIGHT = '70px'
export const useGetHeaderHeight = () => {
const [hidePinnedSearches] = usePersistedState({
key: '--library-hide-pinned-searches',
initialValue: false,
isSessionStorage: false,
})
const [pinnedSearches] = usePersistedState<PinnedSearch[] | null>({
key: `--library-pinned-searches`,
initialValue: [],
isSessionStorage: false,
})
if (hidePinnedSearches || !pinnedSearches?.length) {
return '70px'
}
return '100px'
}
export function HeaderSpacer(): JSX.Element {
const headerHeight = useGetHeaderHeight()
return (
<Box
css={{
height: HEADER_HEIGHT,
height: headerHeight,
bg: '$grayBase',
}}
></Box>

View File

@ -13,7 +13,6 @@ import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { MenuTrigger } from '../../elements/MenuTrigger'
import { StyledText } from '../../elements/StyledText'
import {
MetaStyle,
timeAgo,
@ -21,7 +20,7 @@ import {
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
import { NotebookContent } from '../article/Notebook'
import { EmptyHighlights } from './EmptyHighlights'
import { HEADER_HEIGHT } from './HeaderSpacer'
import { useGetHeaderHeight } from './HeaderSpacer'
import { highlightsAsMarkdown } from './HighlightItem'
type HighlightItemsLayoutProps = {
@ -34,8 +33,10 @@ type HighlightItemsLayoutProps = {
export function HighlightItemsLayout(
props: HighlightItemsLayoutProps
): JSX.Element {
const [currentItem, setCurrentItem] =
useState<LibraryItem | undefined>(undefined)
const headerHeight = useGetHeaderHeight()
const [currentItem, setCurrentItem] = useState<LibraryItem | undefined>(
undefined
)
const listReducer = (
state: LibraryItem[],
@ -105,7 +106,7 @@ export function HighlightItemsLayout(
<Box
css={{
width: '100%',
height: `calc(100vh - ${HEADER_HEIGHT})`,
height: `calc(100vh - ${headerHeight})`,
}}
>
<EmptyHighlights />
@ -118,7 +119,7 @@ export function HighlightItemsLayout(
<HStack
css={{
width: '100%',
height: `calc(100vh - ${HEADER_HEIGHT})`,
height: `calc(100vh - ${headerHeight})`,
'@lgDown': {
overflowY: 'scroll',
},
@ -146,7 +147,7 @@ export function HighlightItemsLayout(
>
<VStack
css={{
minHeight: `calc(100vh - ${HEADER_HEIGHT})`,
minHeight: `calc(100vh - ${headerHeight})`,
bg: '$thBackground',
}}
distribution="start"

View File

@ -51,6 +51,8 @@ import { NotebookPresenter } from '../article/NotebookPresenter'
import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation'
import { articleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
import { searchQuery } from '../../../lib/networking/queries/search'
import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon'
import { theme } from '../../tokens/stitches.config'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
export type LibraryMode = 'reads' | 'highlights'

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { theme } from '../../tokens/stitches.config'
import { FormInput } from '../../elements/FormElements'
@ -16,7 +16,7 @@ import {
import { LayoutType } from './HomeFeedContainer'
import { PrimaryDropdown } from '../PrimaryDropdown'
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
import { HeaderSpacer, HEADER_HEIGHT } from './HeaderSpacer'
import { HeaderSpacer, useGetHeaderHeight } from './HeaderSpacer'
import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu'
import { CardCheckbox } from '../../patterns/LibraryCards/LibraryCardStyles'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
@ -30,6 +30,9 @@ import { LabelIcon } from '../../elements/icons/LabelIcon'
import { ListViewIcon } from '../../elements/icons/ListViewIcon'
import { GridViewIcon } from '../../elements/icons/GridViewIcon'
import { CaretDownIcon } from '../../elements/icons/CaretDownIcon'
import { PinnedButtons } from './PinnedButtons'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search'
@ -61,7 +64,30 @@ type LibraryHeaderProps = {
) => Promise<void>
}
const controlWidths = (
layout: LayoutType,
multiSelectMode: MultiSelectMode
) => {
return {
width: '95%',
'@mdDown': {
width: multiSelectMode !== 'off' ? '100%' : '95%',
display: multiSelectMode !== 'off' ? 'flex' : 'none',
},
'@media (min-width: 930px)': {
width: layout == 'GRID_LAYOUT' ? '660px' : '640px',
},
'@media (min-width: 1280px)': {
width: '1000px',
},
'@media (min-width: 1600px)': {
width: '1340px',
},
}
}
export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
const headerHeight = useGetHeaderHeight()
return (
<>
<VStack
@ -73,7 +99,7 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
left: LIBRARY_LEFT_MENU_WIDTH,
zIndex: 5,
position: 'fixed',
height: HEADER_HEIGHT,
height: headerHeight,
bg: '$thLibraryBackground',
'@mdDown': {
left: '0px',
@ -94,6 +120,14 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
}
function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
const [pinnedSearches, setPinnedSearches] = usePersistedState<
PinnedSearch[] | null
>({
key: `--library-pinned-searches`,
initialValue: [],
isSessionStorage: false,
})
return (
<HStack
alignment="center"
@ -106,19 +140,40 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
},
}}
>
<ControlButtonBox
layout={props.layout}
updateLayout={props.updateLayout}
numItemsSelected={props.numItemsSelected}
multiSelectMode={props.multiSelectMode}
setMultiSelectMode={props.setMultiSelectMode}
showAddLinkModal={props.showAddLinkModal}
performMultiSelectAction={props.performMultiSelectAction}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
allowSelectMultiple={props.allowSelectMultiple}
handleLinkSubmission={props.handleLinkSubmission}
/>
<VStack alignment="center" distribution="start">
<ControlButtonBox
layout={props.layout}
updateLayout={props.updateLayout}
numItemsSelected={props.numItemsSelected}
multiSelectMode={props.multiSelectMode}
setMultiSelectMode={props.setMultiSelectMode}
showAddLinkModal={props.showAddLinkModal}
performMultiSelectAction={props.performMultiSelectAction}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
allowSelectMultiple={props.allowSelectMultiple}
handleLinkSubmission={props.handleLinkSubmission}
/>
<SpanBox
css={{
...controlWidths(props.layout, props.multiSelectMode),
width: '100%',
maxWidth: '587px',
alignSelf: 'flex-start',
'-ms-overflow-style': 'none',
scrollbarWidth: 'none',
'::-webkit-scrollbar': {
display: 'none',
},
}}
>
<PinnedButtons
items={pinnedSearches ?? []}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
/>
</SpanBox>
</VStack>
</HStack>
)
}
@ -616,20 +671,7 @@ function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element {
distribution={props.multiSelectMode !== 'off' ? 'center' : 'start'}
css={{
gap: '10px',
width: '95%',
'@mdDown': {
width: props.multiSelectMode !== 'off' ? '100%' : '95%',
display: props.multiSelectMode !== 'off' ? 'flex' : 'none',
},
'@media (min-width: 930px)': {
width: props.layout == 'GRID_LAYOUT' ? '660px' : '640px',
},
'@media (min-width: 1280px)': {
width: '1000px',
},
'@media (min-width: 1600px)': {
width: '1340px',
},
...controlWidths(props.layout, props.multiSelectMode),
}}
>
<MuliSelectControl {...props} />

View File

@ -0,0 +1,102 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { theme } from '../../tokens/stitches.config'
import { Button } from '../../elements/Button'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { useRouter } from 'next/router'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
type PinnedButtonsProps = {
items: PinnedSearch[]
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
}
export const PinnedButtons = (props: PinnedButtonsProps): JSX.Element => {
const router = useRouter()
const [hidePinnedSearches, setHidePinnedSearches] = usePersistedState({
key: '--library-hide-pinned-searches',
initialValue: false,
isSessionStorage: false,
})
if (hidePinnedSearches || !props.items.length) {
return <></>
}
return (
<HStack
alignment="center"
distribution="start"
css={{
width: '100%',
maxWidth: '100%',
pt: '10px',
pb: '0px',
gap: '10px',
bg: 'transparent',
overflowX: 'scroll',
}}
>
{props.items.map((item) => {
const style =
item.search == props.searchTerm ? 'ctaPill' : 'ctaPillUnselected'
return (
<Button
key={item.search}
style={style}
onClick={(event) => {
props.applySearchQuery(item.search)
event.preventDefault()
}}
>
{item.name}
</Button>
)
})}
<Dropdown
triggerElement={
<SpanBox
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
width: '24px',
height: '24px',
border: '1px solid $thBackground4',
backgroundColor: '$thBackground4',
'&:hover': {
bg: '$grayBgHover',
border: '1px solid $grayBgHover',
},
}}
>
<MoreOptionsIcon
size={16}
strokeColor={theme.colors.grayText.toString()}
orientation={'horizontal'}
/>
</SpanBox>
}
css={{}}
>
<DropdownOption
onSelect={() => {
router.push('/settings/pinned-searches')
}}
title="Edit"
/>
<DropdownOption
onSelect={() => {
setHidePinnedSearches(true)
}}
title="Hide"
/>
</Dropdown>
</HStack>
)
}

View File

@ -1,10 +1,9 @@
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { PrimaryDropdown } from '../PrimaryDropdown'
import { TooltipWrapped } from '../../elements/Tooltip'
import { LogoBox } from '../../elements/LogoBox'
import { ReactNode } from 'react'
import { HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { useGetHeaderHeight } from '../homeFeed/HeaderSpacer'
import { theme } from '../../tokens/stitches.config'
import { ReaderSettingsIcon } from '../../elements/icons/ReaderSettingsIcon'
import { CircleUtilityMenuIcon } from '../../elements/icons/CircleUtilityMenuIcon'
@ -17,6 +16,7 @@ type ReaderHeaderProps = {
}
export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
const headerHeight = useGetHeaderHeight()
return (
<>
<VStack
@ -29,7 +29,7 @@ export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
pt: '0px',
position: 'fixed',
width: '100%',
height: HEADER_HEIGHT,
height: headerHeight,
display: props.alwaysDisplayToolbar ? 'flex' : 'transparent',
pointerEvents: props.alwaysDisplayToolbar ? 'unset' : 'none',
borderBottom: '1px solid transparent',

View File

@ -0,0 +1,375 @@
import React, {
ReactNode,
useCallback,
useEffect,
useMemo,
useReducer,
} from 'react'
import { Toaster } from 'react-hot-toast'
import {
Box,
HStack,
SpanBox,
VStack,
} from '../../components/elements/LayoutPrimitives'
import { StyledText } from '../../components/elements/StyledText'
import { SettingsLayout } from '../../components/templates/SettingsLayout'
import { applyStoredTheme } from '../../lib/themeUpdater'
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
import { Label } from '../../lib/networking/fragments/labelFragment'
import { Circle } from 'phosphor-react'
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
import { usePersistedState } from '../../lib/hooks/usePersistedState'
export type PinnedSearch = {
type: 'saved-search' | 'label'
itemId: string
name: string
search: string
}
const PINNED_SEARCHES_KEY = `--library-pinned-searches`
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
export default function PinnedSearches(): JSX.Element {
const { labels } = useGetLabelsQuery()
const { savedSearches } = useGetSavedSearchQuery()
const [hidePinnedSearches, setHidePinnedSearches] = usePersistedState({
key: '--library-hide-pinned-searches',
initialValue: false,
isSessionStorage: false,
})
const listReducer = (
state: { state: string; items: PinnedSearch[] },
action: {
type: ListAction
item?: PinnedSearch
}
) => {
switch (action.type) {
case 'RESET': {
const itemStr = window['localStorage'].getItem(PINNED_SEARCHES_KEY)
if (itemStr) {
try {
const parsed = JSON.parse(itemStr)
if (Array.isArray(parsed)) {
return { state: 'CURRENT', items: parsed as PinnedSearch[] }
}
} catch (err) {
console.log('error: ', err)
}
}
return { state: 'CURRENT', items: [] }
}
case 'ADD_ITEM': {
const item = action.item
if (!item) {
return state
}
const existing = state.items.find(
(existing) =>
existing.type == item.type && existing.itemId == item.itemId
)
if (existing) {
return state
}
state.items.push(item)
return { state: 'CURRENT', items: [...state.items] }
}
case 'REMOVE_ITEM': {
const item = action.item
if (!item) {
return state
}
const updated = state.items.filter(
(existing) => existing.itemId != item.itemId
)
return { state: 'CURRENT', items: [...updated] }
}
default:
throw new Error('unknown action')
}
}
const [pinnedSearches, dispatchList] = useReducer(listReducer, {
state: 'INITIAL',
items: [],
})
const items = useMemo(() => {
if (pinnedSearches.state == 'INITIAL') {
return { labelItems: [], savedSearchItems: [] }
}
const labelItems = labels.map((label) => {
return {
label,
isSelected: !!pinnedSearches.items.find(
(ps) => ps.type == 'label' && ps.itemId == label.id
),
}
})
const savedSearchItems = (savedSearches ?? []).map((savedSearch) => {
return {
savedSearch,
isSelected: !!pinnedSearches.items.find(
(ps) => ps.type == 'saved-search' && ps.itemId == savedSearch.id
),
}
})
return { labelItems, savedSearchItems }
}, [pinnedSearches, labels, savedSearches])
useEffect(() => {
try {
// Only write updated state to local storage
if (pinnedSearches.state == 'CURRENT') {
window['localStorage'].setItem(
PINNED_SEARCHES_KEY,
JSON.stringify(pinnedSearches.items)
)
}
} catch (error) {
console.log('error": ', error)
}
}, [pinnedSearches])
useEffect(() => {
dispatchList({ type: 'RESET' })
}, [])
applyStoredTheme(false)
return (
<SettingsLayout>
<Toaster
containerStyle={{
top: '5rem',
}}
/>
<VStack
css={{ width: '100%', height: '100%' }}
distribution="start"
alignment="center"
>
<VStack
css={{
padding: '24px',
width: '100%',
height: '100%',
gap: '25px',
minWidth: '300px',
maxWidth: '865px',
}}
>
<Box>
<StyledText style="fixedHeadline" css={{ my: '6px' }}>
Pinned Searches
</StyledText>
<StyledText style="caption" css={{}}>
Pin up to five searches from your labels or saved searches.
</StyledText>
</Box>
<VStack
css={{
padding: '24px',
width: '100%',
height: '100%',
bg: '$grayBg',
gap: '5px',
borderRadius: '5px',
}}
distribution="start"
alignment="start"
>
<HStack alignment="center" css={{ gap: '5px', mb: '10px' }}>
<input
type="checkbox"
id="switch"
checked={!hidePinnedSearches}
onChange={(event) => {
setHidePinnedSearches(!event.currentTarget.checked)
}}
style={{ padding: '0px', margin: '0px' }}
/>
Enable Pinned Searches
</HStack>
{!hidePinnedSearches && (
<>
<StyledText style="modalTitle" css={{}}>
Saved Searches
</StyledText>
{items.savedSearchItems.map((item) => {
return (
<SearchButton
key={`search-${item.savedSearch.id}`}
search={item.savedSearch}
isSelected={item.isSelected}
listAction={dispatchList}
/>
)
})}
<StyledText style="modalTitle" css={{ mt: '20px' }}>
Labels
</StyledText>
{items.labelItems.map((item) => {
return (
<LabelButton
label={item.label}
key={`label-${item.label.id}`}
listAction={dispatchList}
isSelected={item.isSelected}
/>
)
})}
</>
)}
</VStack>
</VStack>
</VStack>
</SettingsLayout>
)
}
type LabelButtonProps = {
label: Label
isSelected: boolean
listAction: (arg: {
type: ListAction
item?: PinnedSearch | undefined
}) => void
}
function LabelButton(props: LabelButtonProps): JSX.Element {
const labelId = `checkbox-label-${props.label.id}`
return (
<CheckboxButton
key={labelId}
title={props.label.name}
isSelected={props.isSelected}
item={{
type: 'label',
itemId: props.label.id,
name: props.label.name,
search: `label:\"${props.label.name}\"`,
}}
listAction={props.listAction}
>
<input
type="checkbox"
checked={props.isSelected}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange={(event) => {}}
/>
<SpanBox css={{}}>{props.label.name}</SpanBox>
<Circle size={9} color={props.label.color} weight="fill" />
</CheckboxButton>
)
}
type SearchButtonProps = {
search: SavedSearch
isSelected: boolean
listAction: (arg: {
type: ListAction
item?: PinnedSearch | undefined
}) => void
}
function SearchButton(props: SearchButtonProps): JSX.Element {
const searchId = `checkbox-search-${props.search.id}`
return (
<CheckboxButton
key={searchId}
title={props.search.filter}
isSelected={props.isSelected}
item={{
type: 'saved-search',
itemId: props.search.id,
name: props.search.name,
search: props.search.filter,
}}
listAction={props.listAction}
>
<input
type="checkbox"
checked={props.isSelected}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange={(e) => {}}
/>
<SpanBox css={{}}>{props.search.name}</SpanBox>
</CheckboxButton>
)
}
type CheckboxButtonProps = {
key: string
title: string
isSelected: boolean
item: PinnedSearch
listAction: (arg: {
type: ListAction
item?: PinnedSearch | undefined
}) => void
children: ReactNode
}
function CheckboxButton(props: CheckboxButtonProps): JSX.Element {
const handleChange = useCallback(
(selected: boolean) => {
if (!selected) {
props.listAction({
type: 'REMOVE_ITEM',
item: props.item,
})
} else {
props.listAction({
type: 'ADD_ITEM',
item: props.item,
})
}
},
[props]
)
return (
<HStack
key={props.key}
title={props.title}
css={{
px: '10px',
pt: '2px',
height: '30px',
gap: '5px',
fontSize: '14px',
fontWeight: 'regular',
fontFamily: '$display',
color: props.isSelected
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
m: '0px',
'&:hover': {
backgroundColor: '$thBackground4',
},
}}
alignment="center"
distribution="start"
onClick={(event) => {
handleChange(!props.isSelected)
event.preventDefault()
}}
>
{props.children}
</HStack>
)
}