From ea734db683eedf57c5b9d8a67b6caf020a94bd4b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 24 Feb 2023 14:54:37 +0800 Subject: [PATCH] Update the library layout to be closer to the design spec --- .../elements/images/GridSelectorIcon.tsx | 48 ++ .../elements/images/ListSelectorIcon.tsx | 44 + .../elements/images/OmnivoreFullLogo.tsx | 61 ++ .../elements/images/OmnivoreNameLogo.tsx | 18 +- .../components/templates/PrimaryLayout.tsx | 41 +- .../templates/homeFeed/HomeFeedContainer.tsx | 769 +++++++++--------- .../templates/homeFeed/LibraryFilterMenu.tsx | 219 +++++ .../templates/homeFeed/LibraryHeader.tsx | 281 +++++++ 8 files changed, 1088 insertions(+), 393 deletions(-) create mode 100644 packages/web/components/elements/images/GridSelectorIcon.tsx create mode 100644 packages/web/components/elements/images/ListSelectorIcon.tsx create mode 100644 packages/web/components/elements/images/OmnivoreFullLogo.tsx create mode 100644 packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx create mode 100644 packages/web/components/templates/homeFeed/LibraryHeader.tsx diff --git a/packages/web/components/elements/images/GridSelectorIcon.tsx b/packages/web/components/elements/images/GridSelectorIcon.tsx new file mode 100644 index 000000000..553e26fd9 --- /dev/null +++ b/packages/web/components/elements/images/GridSelectorIcon.tsx @@ -0,0 +1,48 @@ +import { config } from '../../tokens/stitches.config' + +export type GridSelectorIconProps = { + color?: string +} + +export function GridSelectorIcon(props: GridSelectorIconProps): JSX.Element { + const fillColor = props.color || config.theme.colors.graySolid + + return ( + + + + + + + + + + + + + + ) +} diff --git a/packages/web/components/elements/images/ListSelectorIcon.tsx b/packages/web/components/elements/images/ListSelectorIcon.tsx new file mode 100644 index 000000000..130e20309 --- /dev/null +++ b/packages/web/components/elements/images/ListSelectorIcon.tsx @@ -0,0 +1,44 @@ +import { config } from '../../tokens/stitches.config' + +export type ListSelectorIconProps = { + color?: string +} + +export function ListSelectorIcon(props: ListSelectorIconProps): JSX.Element { + const fillColor = props.color || config.theme.colors.graySolid + + return ( + + + + + + + + + + + + + ) +} diff --git a/packages/web/components/elements/images/OmnivoreFullLogo.tsx b/packages/web/components/elements/images/OmnivoreFullLogo.tsx new file mode 100644 index 000000000..89d9ded0b --- /dev/null +++ b/packages/web/components/elements/images/OmnivoreFullLogo.tsx @@ -0,0 +1,61 @@ +import { config } from '../../tokens/stitches.config' + +export type OmnivoreFullLogoProps = { + color?: string + href?: string + showTitle?: boolean +} + +export function OmnivoreFullLogo(props: OmnivoreFullLogoProps): JSX.Element { + const fillColor = props.color || config.theme.colors.graySolid + const href = props.href || '/home' + + return ( + + + + + + + + + + + + ) +} diff --git a/packages/web/components/elements/images/OmnivoreNameLogo.tsx b/packages/web/components/elements/images/OmnivoreNameLogo.tsx index 0f3e25328..f32804b9b 100644 --- a/packages/web/components/elements/images/OmnivoreNameLogo.tsx +++ b/packages/web/components/elements/images/OmnivoreNameLogo.tsx @@ -42,6 +42,7 @@ export function OmnivoreLogoIcon(props: OmnivoreLogoProps): JSX.Element { export type OmnivoreNameLogoProps = { color?: string href?: string + showTitle?: boolean } export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element { @@ -50,9 +51,22 @@ export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element { return ( - + - {/* Omnivore */} + {props.showTitle && ( + + Omnivore + + )} ) diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index 30b652ea3..32fab35df 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -1,11 +1,6 @@ import { PageMetaData, PageMetaDataProps } from '../patterns/PageMetaData' import { Box } from '../elements/LayoutPrimitives' -import { - ReactNode, - MutableRefObject, - useEffect, - useState, -} from 'react' +import { ReactNode, MutableRefObject, useEffect, useState } from 'react' import { PrimaryHeader } from './../patterns/PrimaryHeader' import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery' import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts' @@ -61,15 +56,17 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { {props.pageMetaDataProps ? ( ) : null} - - + {/* + /> */} - + {/* */} {props.children} {showLogoutConfirmation ? ( - + + - + > + + + - {props.isValidating && props.items.length == 0 && } - + + + + {props.isValidating && props.items.length == 0 && } + {/* + /> */} - {viewerData?.me && ( - + {Object.keys(SAVED_SEARCHES).map((key) => { + const isInboxTerm = (term: string) => { + return !term || term === 'in:inbox' + } + + const searchQuery = SAVED_SEARCHES[key] + const style = + searchQuery === props.searchTerm || + (!props.searchTerm && isInboxTerm(searchQuery)) + ? 'ctaDarkYellow' + : 'ctaLightGray' + return ( + + ) + })} + + )} */} + { + setInDragOperation(true) + }} + onDragLeave={() => { + setInDragOperation(false) + }} + preventDropOnDocument={true} + noClick={true} + accept={{ + 'application/pdf': ['.pdf'], }} > - {Object.keys(SAVED_SEARCHES).map((key) => { - const isInboxTerm = (term: string) => { - return !term || term === 'in:inbox' - } - - const searchQuery = SAVED_SEARCHES[key] - const style = - searchQuery === props.searchTerm || - (!props.searchTerm && isInboxTerm(searchQuery)) - ? 'ctaDarkYellow' - : 'ctaLightGray' - return ( - - ) - })} - - )} - { - setInDragOperation(true) - }} - onDragLeave={() => { - setInDragOperation(false) - }} - preventDropOnDocument={true} - noClick={true} - accept={{ - 'application/pdf': ['.pdf'], - }} - > - {({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => ( -
- {inDragOperation && uploadingFiles.length < 1 && ( - - - - Drop PDF document to to upload and add to your library - - - - )} - {uploadingFiles.length > 0 && ( - - - - - - - ( +
+ {inDragOperation && uploadingFiles.length < 1 && ( + + + - Uploading file - - - - - )} - - {!props.isValidating && props.items.length == 0 ? ( - { - props.setShowAddLinkModal(true) - }} - /> - ) : ( - - {props.items.map((linkedItem) => ( - div': { - bg: '$grayBg', - }, - '&:focus': { - '> div': { - bg: '$grayBgActive', - }, - }, - '&:hover': { - '> div': { - bg: '$grayBgActive', - }, - }, - }} - > - {viewerData?.me && ( - { - if (action === 'delete') { - setShowRemoveLinkConfirmation(true) - props.setLinkToRemove(linkedItem) - } else if (action === 'editTitle') { - props.setShowEditTitleModal(true) - props.setLinkToEdit(linkedItem) - } else if (action == 'unsubscribe') { - setShowUnsubscribeConfirmation(true) - props.setLinkToUnsubscribe(linkedItem) - } else { - props.actionHandler(action, linkedItem) - } - }} - /> - )} - - ))} - - )} - - {props.hasMore ? ( - - ) : ( - + Drop PDF document to to upload and add to your library + + + )} - -
- )} - - - {/* Temporary code */} - {/*
+ {uploadingFiles.length > 0 && ( + + + + + + + + Uploading file + + + + + )} + + {!props.isValidating && props.items.length == 0 ? ( + { + props.setShowAddLinkModal(true) + }} + /> + ) : ( + + {props.items.map((linkedItem) => ( + div': { + bg: '$grayBg', + }, + '&:focus': { + '> div': { + bg: '$grayBgActive', + }, + }, + '&:hover': { + '> div': { + bg: '$grayBgActive', + }, + }, + }} + > + {viewerData?.me && ( + { + if (action === 'delete') { + setShowRemoveLinkConfirmation(true) + props.setLinkToRemove(linkedItem) + } else if (action === 'editTitle') { + props.setShowEditTitleModal(true) + props.setLinkToEdit(linkedItem) + } else if (action == 'unsubscribe') { + setShowUnsubscribeConfirmation(true) + props.setLinkToUnsubscribe(linkedItem) + } else { + props.actionHandler(action, linkedItem) + } + }} + /> + )} + + ))} + + )} + + {props.hasMore ? ( + + ) : ( + + )} + +
+ )} + + + {/* Temporary code */} + {/*
Files:
    {uploadingFiles.map((fileName) => ( @@ -1093,142 +1123,143 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { ))}
*/} - {/* Temporary code */} - {props.showAddLinkModal && ( - props.setShowAddLinkModal(false)} /> - )} - {props.showEditTitleModal && ( - - props.actionHandler('update-item', item) - } - onOpenChange={() => props.setShowEditTitleModal(false)} - item={props.linkToEdit as LibraryItem} - /> - )} - {props.shareTarget && viewerData?.me?.profile.username && ( - { - if (props.shareTarget) { - const item = document.getElementById(props.shareTarget.node.id) - if (item) { - item.focus() + {/* Temporary code */} + {props.showAddLinkModal && ( + props.setShowAddLinkModal(false)} /> + )} + {props.showEditTitleModal && ( + + props.actionHandler('update-item', item) + } + onOpenChange={() => props.setShowEditTitleModal(false)} + item={props.linkToEdit as LibraryItem} + /> + )} + {props.shareTarget && viewerData?.me?.profile.username && ( + { + if (props.shareTarget) { + const item = document.getElementById(props.shareTarget.node.id) + if (item) { + item.focus() + } + props.setShareTarget(undefined) } - props.setShareTarget(undefined) - } - }} - /> - )} - {props.snoozeTarget && ( - { - if (!props.snoozeTarget) return - createReminderMutation( - props.snoozeTarget?.node.id, - ReminderType.Tonight, - true, - sendReminder - ) - .then(() => { - return props.actionHandler('archive', props.snoozeTarget) - }) - .then(() => { - showSuccessToast(msg, { position: 'bottom-right' }) - }) - .catch((error) => { - showErrorToast('There was an error snoozing your link.', { - position: 'bottom-right', - }) - }) - }} - onOpenChange={() => { - if (props.snoozeTarget) { - const item = document.getElementById(props.snoozeTarget.node.id) - if (item) { - item.focus() - } - props.setSnoozeTarget(undefined) - } - }} - /> - )} - {showRemoveLinkConfirmation && ( - - - Are you sure you want to delete this item? All associated notes - and highlights will be deleted. - - {props.linkToRemove?.node && viewerData?.me && ( - - {}} - /> - - )} - - } - onAccept={removeItem} - acceptButtonLabel="Delete Item" - onOpenChange={() => setShowRemoveLinkConfirmation(false)} - /> - )} - {showUnsubscribeConfirmation && ( - setShowUnsubscribeConfirmation(false)} - /> - )} - {props.labelsTarget?.node.id && ( - { - if (props.labelsTarget) { - props.labelsTarget.node.labels = labels - updateState({}) - } - }} - save={(labels: Label[]) => { - if (props.labelsTarget?.node.id) { - return setLabelsMutation( - props.labelsTarget.node.id, - labels.map((label) => label.id) + }} + /> + )} + {props.snoozeTarget && ( + { + if (!props.snoozeTarget) return + createReminderMutation( + props.snoozeTarget?.node.id, + ReminderType.Tonight, + true, + sendReminder ) + .then(() => { + return props.actionHandler('archive', props.snoozeTarget) + }) + .then(() => { + showSuccessToast(msg, { position: 'bottom-right' }) + }) + .catch((error) => { + showErrorToast('There was an error snoozing your link.', { + position: 'bottom-right', + }) + }) + }} + onOpenChange={() => { + if (props.snoozeTarget) { + const item = document.getElementById(props.snoozeTarget.node.id) + if (item) { + item.focus() + } + props.setSnoozeTarget(undefined) + } + }} + /> + )} + {showRemoveLinkConfirmation && ( + + + Are you sure you want to delete this item? All associated + notes and highlights will be deleted. + + {props.linkToRemove?.node && viewerData?.me && ( + + {}} + /> + + )} + } - return Promise.resolve(undefined) - }} - onOpenChange={() => { - if (props.labelsTarget) { - const activate = props.labelsTarget - props.setActiveItem(activate) - props.setLabelsTarget(undefined) - } - }} - /> - )} - + onAccept={removeItem} + acceptButtonLabel="Delete Item" + onOpenChange={() => setShowRemoveLinkConfirmation(false)} + /> + )} + {showUnsubscribeConfirmation && ( + setShowUnsubscribeConfirmation(false)} + /> + )} + {props.labelsTarget?.node.id && ( + { + if (props.labelsTarget) { + props.labelsTarget.node.labels = labels + updateState({}) + } + }} + save={(labels: Label[]) => { + if (props.labelsTarget?.node.id) { + return setLabelsMutation( + props.labelsTarget.node.id, + labels.map((label) => label.id) + ) + } + return Promise.resolve(undefined) + }} + onOpenChange={() => { + if (props.labelsTarget) { + const activate = props.labelsTarget + props.setActiveItem(activate) + props.setLabelsTarget(undefined) + } + }} + /> + )} + + ) } diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx new file mode 100644 index 000000000..6e9e10d35 --- /dev/null +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -0,0 +1,219 @@ +import { + InputHTMLAttributes, + ReactNode, + 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 } 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 { Circle, MagnifyingGlass, Plus, 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 { useGetSubscriptionsQuery } from '../../../lib/networking/queries/useGetSubscriptionsQuery' +import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' +import { Label } from '../../../lib/networking/fragments/labelFragment' +import { Checkbox } from '@radix-ui/react-checkbox' + +export function LibraryFilterMenu(): JSX.Element { + return ( + + + + + + + + ) +} + +function SavedSearches(): JSX.Element { + return ( + + + + + + + + ) +} + +function Subscriptions(): JSX.Element { + const { subscriptions } = useGetSubscriptionsQuery() + console.log('subscriptions: ', subscriptions) + + return ( + + {subscriptions.slice(0, 4).map((item) => { + return + })} + + View All + + + ) +} + +function Labels(): JSX.Element { + const { labels } = useGetLabelsQuery() + console.log('labels: ', labels) + + return ( + + {labels.slice(0, 4).map((item) => { + return + })} + + View All + + + ) +} + +type MenuPanelProps = { + title: string + children: ReactNode +} + +function MenuPanel(props: MenuPanelProps): JSX.Element { + return ( + + + {props.title} + + {props.children} + + ) +} + +type FilterButtonProps = { + text: string + spaced?: boolean + selected: boolean +} + +function FilterButton(props: FilterButtonProps): JSX.Element { + return ( + + {props.text} + + ) +} + +type LabelButtonProps = { + label: Label + state: 'on' | 'off' | 'unset' +} + +function LabelButton(props: LabelButtonProps): JSX.Element { + return ( + + + {props.label.name} + + + + + ) +} + +function AddLinkButton(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx new file mode 100644 index 000000000..6bf65a93c --- /dev/null +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -0,0 +1,281 @@ +import { + InputHTMLAttributes, + ReactNode, + 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 } 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' + +type LibrarySearchBarProps = { + searchTerm?: string + applySearchQuery: (searchQuery: string) => void +} + +type LibraryFilter = + | 'in:inbox' + | 'in:all' + | 'in:archive' + | 'type:file' + | 'type:highlights' + | `saved:${string}` + | `sort:read` + +// get last week's date +const recentlySavedStartDate = new Date( + new Date().getTime() - 7 * 24 * 60 * 60 * 1000 +).toLocaleDateString('en-US') + +const FOCUSED_BOXSHADOW = '0px 0px 2px 2px rgba(255, 234, 159, 0.56)' + +export function LibraryHeader(props: LibrarySearchBarProps): JSX.Element { + const [focused, setFocused] = useState(false) + const inputRef = useRef(null) + const [searchTerm, setSearchTerm] = useState(props.searchTerm || '') + + useEffect(() => { + setSearchTerm(props.searchTerm || '') + }, [props.searchTerm]) + + useKeyboardShortcuts( + searchBarCommands((action) => { + if (action === 'focusSearchBar' && inputRef.current) { + inputRef.current.select() + } + }) + ) + + return ( + + + + + + + + ) +} + +function SearchBox(props: LibrarySearchBarProps): JSX.Element { + const inputRef = useRef(null) + const [focused, setFocused] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + + return ( + + + + + +
{ + event.preventDefault() + props.applySearchQuery(searchTerm || '') + inputRef.current?.blur() + }} + > + { + event.target.select() + setFocused(true) + }} + onBlur={() => { + setFocused(false) + }} + onChange={(event) => { + setSearchTerm(event.target.value) + }} + /> + + {searchTerm ? ( + + ) : ( + + requestAnimationFrame(() => inputRef.current.focus())} + // we can make it unreachable via keyboard as we have the same message for the SR label + tabIndex={-1} + > + / + + + )} +
+
+ ) +} + +// Displays the full logo on larger screens, small logo on mobile +function LogoBox(): JSX.Element { + return ( + <> + + + + + + + + ) +} + +function ControlButtonBox(): JSX.Element { + return ( + <> + + + + + + + + + + + ) +}