diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 506fd869b..eea66f75a 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -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', diff --git a/packages/web/components/patterns/SettingsHeader.tsx b/packages/web/components/patterns/SettingsHeader.tsx index e359126ec..b8c218a08 100644 --- a/packages/web/components/patterns/SettingsHeader.tsx +++ b/packages/web/components/patterns/SettingsHeader.tsx @@ -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', }, diff --git a/packages/web/components/templates/PrimaryDropdown.tsx b/packages/web/components/templates/PrimaryDropdown.tsx index 5d46f9932..f28aaa415 100644 --- a/packages/web/components/templates/PrimaryDropdown.tsx +++ b/packages/web/components/templates/PrimaryDropdown.tsx @@ -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', diff --git a/packages/web/components/templates/SettingsLayout.tsx b/packages/web/components/templates/SettingsLayout.tsx index f10457ce1..a7df5dedd 100644 --- a/packages/web/components/templates/SettingsLayout.tsx +++ b/packages/web/components/templates/SettingsLayout.tsx @@ -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 { diff --git a/packages/web/components/templates/SettingsMenu.tsx b/packages/web/components/templates/SettingsMenu.tsx index cfe9d60c1..5663ae6d6 100644 --- a/packages/web/components/templates/SettingsMenu.tsx +++ b/packages/web/components/templates/SettingsMenu.tsx @@ -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} - ); + ) } diff --git a/packages/web/components/templates/article/EpubContainer.tsx b/packages/web/components/templates/article/EpubContainer.tsx index 85a21ad84..c4c25623d 100644 --- a/packages/web/components/templates/article/EpubContainer.tsx +++ b/packages/web/components/templates/article/EpubContainer.tsx @@ -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})`, }} >
diff --git a/packages/web/components/templates/homeFeed/EmptyHighlights.tsx b/packages/web/components/templates/homeFeed/EmptyHighlights.tsx index 030444c09..1972ee1be 100644 --- a/packages/web/components/templates/homeFeed/EmptyHighlights.tsx +++ b/packages/web/components/templates/homeFeed/EmptyHighlights.tsx @@ -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 ( diff --git a/packages/web/components/templates/homeFeed/HeaderSpacer.tsx b/packages/web/components/templates/homeFeed/HeaderSpacer.tsx index 080a5f43c..43671f53e 100644 --- a/packages/web/components/templates/homeFeed/HeaderSpacer.tsx +++ b/packages/web/components/templates/homeFeed/HeaderSpacer.tsx @@ -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({ + key: `--library-pinned-searches`, + initialValue: [], + isSessionStorage: false, + }) + + if (hidePinnedSearches || !pinnedSearches?.length) { + return '70px' + } + return '100px' +} export function HeaderSpacer(): JSX.Element { + const headerHeight = useGetHeaderHeight() return ( diff --git a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx index 75ffcf5d0..12a16849f 100644 --- a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx +++ b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx @@ -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(undefined) + const headerHeight = useGetHeaderHeight() + const [currentItem, setCurrentItem] = useState( + undefined + ) const listReducer = ( state: LibraryItem[], @@ -105,7 +106,7 @@ export function HighlightItemsLayout( @@ -118,7 +119,7 @@ export function HighlightItemsLayout( Promise } +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 ( <> ({ + key: `--library-pinned-searches`, + initialValue: [], + isSessionStorage: false, + }) + return ( - + + + + + + ) } @@ -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), }} > diff --git a/packages/web/components/templates/homeFeed/PinnedButtons.tsx b/packages/web/components/templates/homeFeed/PinnedButtons.tsx new file mode 100644 index 000000000..5b62ad40c --- /dev/null +++ b/packages/web/components/templates/homeFeed/PinnedButtons.tsx @@ -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 ( + + {props.items.map((item) => { + const style = + item.search == props.searchTerm ? 'ctaPill' : 'ctaPillUnselected' + return ( + + ) + })} + + + + } + css={{}} + > + { + router.push('/settings/pinned-searches') + }} + title="Edit" + /> + { + setHidePinnedSearches(true) + }} + title="Hide" + /> + + + ) +} diff --git a/packages/web/components/templates/reader/ReaderHeader.tsx b/packages/web/components/templates/reader/ReaderHeader.tsx index 011b9ddd7..76287962a 100644 --- a/packages/web/components/templates/reader/ReaderHeader.tsx +++ b/packages/web/components/templates/reader/ReaderHeader.tsx @@ -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 ( <> { + 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 ( + + + + + + + Pinned Searches + + + Pin up to five searches from your labels or saved searches. + + + + + + { + setHidePinnedSearches(!event.currentTarget.checked) + }} + style={{ padding: '0px', margin: '0px' }} + /> + Enable Pinned Searches + + + {!hidePinnedSearches && ( + <> + + Saved Searches + + {items.savedSearchItems.map((item) => { + return ( + + ) + })} + + + Labels + + {items.labelItems.map((item) => { + return ( + + ) + })} + + )} + + + + + ) +} + +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 ( + + {}} + /> + {props.label.name} + + + ) +} + +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 ( + + {}} + /> + {props.search.name} + + ) +} + +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 ( + { + handleChange(!props.isSelected) + event.preventDefault() + }} + > + {props.children} + + ) +}