diff --git a/packages/web/components/elements/images/SideBarIcon.tsx b/packages/web/components/elements/images/SideBarIcon.tsx new file mode 100644 index 000000000..908b3d740 --- /dev/null +++ b/packages/web/components/elements/images/SideBarIcon.tsx @@ -0,0 +1,20 @@ +type SideBarProps = { + strokeColor: string +} + +export function SideBarIcon(props: SideBarProps): JSX.Element { + return ( + + + + ) +} diff --git a/packages/web/components/templates/article/NotebookModal.tsx b/packages/web/components/templates/article/NotebookModal.tsx index e676db06a..42986fb55 100644 --- a/packages/web/components/templates/article/NotebookModal.tsx +++ b/packages/web/components/templates/article/NotebookModal.tsx @@ -242,7 +242,7 @@ type TextEditAreaProps = { updateHighlight: (highlight: Highlight) => void } -const TextEditArea = (props: TextEditAreaProps): JSX.Element => { +export const TextEditArea = (props: TextEditAreaProps): JSX.Element => { const [noteContent, setNoteContent] = useState( props.highlight.annotation ?? '' ) diff --git a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx new file mode 100644 index 000000000..4ae1987c7 --- /dev/null +++ b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx @@ -0,0 +1,390 @@ +import dayjs from 'dayjs' +import { DotsThreeVertical, HighlighterCircle } from 'phosphor-react' +import { Fragment, useEffect, useMemo, useState } from 'react' +import { Label } from '../../../lib/networking/fragments/labelFragment' +import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import { SideBarIcon } from '../../elements/images/SideBarIcon' +import { LabelChip } from '../../elements/LabelChip' +import { + Blockquote, + Box, + HStack, + SpanBox, + VStack, +} from '../../elements/LayoutPrimitives' +import { StyledText } from '../../elements/StyledText' +import { MetaStyle } from '../../patterns/LibraryCards/LibraryCardStyles' +import { styled, theme } from '../../tokens/stitches.config' +import { TextEditArea } from '../article/NotebookModal' + +type HighlightItemsLayoutProps = { + items: LibraryItem[] + viewer: UserBasicData | undefined + + gridContainerRef: React.RefObject +} + +export function HighlightItemsLayout( + props: HighlightItemsLayoutProps +): JSX.Element { + const [currentItem, setCurrentItem] = useState( + undefined + ) + + useEffect(() => { + if (!currentItem && props.items.length > 0) { + setCurrentItem(props.items[0]) + } + }, [currentItem, setCurrentItem, props.items]) + + return ( + + + + + + {props.items.map((linkedItem) => ( + { + setCurrentItem(linkedItem) + event.preventDefault() + }} + > + {props.viewer && ( + + )} + + ))} + + {currentItem && ( + + )} + + ) +} + +type HighlightTitleCardProps = { + item: LibraryItem + viewer: UserBasicData + selected: boolean +} + +const timeAgo = (date: string | undefined): string => { + if (!date) { + return '' + } + return dayjs(date).fromNow() +} + +function HighlightTitleCard(props: HighlightTitleCardProps): JSX.Element { + return ( + + + + + + {timeAgo(props.item.node.savedAt)} + {` `} + {props.item.node.wordsCount ?? 0 > 0 + ? ` • ${Math.max( + 1, + Math.round((props.item.node.wordsCount ?? 0) / 235) + )} min read` + : null} + {props.item.node.readingProgressPercent ?? 0 > 0 ? ( + <> + {` • `} + + {`${Math.round(props.item.node.readingProgressPercent)}%`} + + + ) : null} + + + + {props.item.node.title} + + + + + + + ) +} + +type HighlightCountChipProps = { + count: number + selected: boolean +} + +function HighlightCountChip(props: HighlightCountChipProps): JSX.Element { + return ( + + {props.count} + + + ) +} + +type HighlightItemsCardProps = { + item: LibraryItem + viewer: UserBasicData | undefined +} + +function HighlightItemsCard(props: HighlightItemsCardProps): JSX.Element { + return ( + + + + + HIGHLIGHTS + + + + {(props.item.node.highlights ?? []).map((highlight) => ( + + ))} + + + + ) +} + +const StyledQuote = styled(Blockquote, { + margin: '0px 0px 0px 0px', + fontSize: '16px', + lineHeight: '1.50', + color: '#D9D9D9', + paddingLeft: '15px', + borderLeft: '2px solid $omnivoreCtaYellow', +}) + +type HighlightItemCardProps = { + highlight: Highlight +} + +function HighlightItemCard(props: HighlightItemCardProps): JSX.Element { + const [isEditing, setIsEditing] = useState(false) + const [hover, setHover] = useState(false) + + const lines = useMemo( + () => props.highlight.quote.split('\n'), + [props.highlight.quote] + ) + + console.log('hover: ', hover) + + return ( + setHover(true)} + onMouseLeave={() => setHover(false)} + > + + + + {lines.map((line: string, index: number) => ( + + {line} + {index !== lines.length - 1 && ( + <> +
+
+ + )} +
+ ))} +
+ + {props.highlight.labels?.map((label: Label, index: number) => ( + + ))} + +
+ + {!isEditing ? ( + setIsEditing(true)} + > + {props.highlight.annotation + ? props.highlight.annotation + : 'Add your notes...'} + + ) : null} + {isEditing && ( + {}} + /> + )} +
+ + + +
+ ) +} diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 51312be3b..100dc5707 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -15,12 +15,9 @@ import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard' import { useRouter } from 'next/router' import { Button } from '../../elements/Button' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { LibrarySearchBar } from './LibrarySearchBar' import { StyledText } from '../../elements/StyledText' import { AddLinkModal } from './AddLinkModal' import { styled, theme } from '../../tokens/stitches.config' -import { ListLayoutIcon } from '../../elements/images/ListLayoutIcon' -import { GridLayoutIcon } from '../../elements/images/GridLayoutIcon' import { libraryListCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts' import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts' import { Toaster } from 'react-hot-toast' @@ -49,22 +46,10 @@ import { uploadFileRequestMutation } from '../../../lib/networking/mutations/upl import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' import { LibraryHeader } from './LibraryHeader' import { LibraryFilterMenu } from './LibraryFilterMenu' +import { HighlightItemsLayout } from './HighlightsLayout' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' - -const timeZoneHourDiff = -new Date().getTimezoneOffset() / 60 - -const SAVED_SEARCHES: Record = { - Inbox: `in:inbox`, - 'Read Later': `in:inbox -label:Newsletter`, - Highlights: `type:highlights`, - Today: `in:inbox saved:${ - new Date(new Date().getTime() - 24 * 3600000).toISOString().split('T')[0] - }Z${timeZoneHourDiff.toLocaleString('en-US', { - signDisplay: 'always', - })}..*`, - Newsletters: `in:inbox label:Newsletter`, -} +export type LibraryMode = 'reads' | 'highlights' const fetchSearchResults = async (query: string, cb: any) => { if (!query.startsWith('#')) return @@ -634,14 +619,11 @@ const DragnDropStyle = styled('div', { function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { const { viewerData } = useGetViewerQuery() + const [mode, setMode] = useState('reads') const [layout, setLayout] = usePersistedState({ key: 'libraryLayout', initialValue: 'GRID_LAYOUT', }) - const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] = - useState(false) - const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] = - useState(false) const updateLayout = useCallback( async (newLayout: LayoutType) => { @@ -651,6 +633,68 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { [layout, setLayout] ) + const [showFilterMenu, setShowFilterMenu] = useState(false) + + return ( + + { + console.log('searching with searchQuery: ', searchQuery) + props.applySearchQuery(searchQuery) + }} + showFilterMenu={showFilterMenu} + setShowFilterMenu={setShowFilterMenu} + /> + + { + console.log('searching with searchQuery: ', searchQuery) + props.applySearchQuery(searchQuery) + }} + showFilterMenu={showFilterMenu} + setShowFilterMenu={setShowFilterMenu} + setMode={setMode} + /> + + {mode == 'highlights' && ( + + )} + + {mode == 'reads' && ( + + )} + + + ) +} + +type LibraryItemsLayoutProps = { + layout: LayoutType + viewer?: UserBasicData +} & HomeFeedContentProps + +function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { + const [uploadingFiles, setUploadingFiles] = useState([]) + const [inDragOperation, setInDragOperation] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + + const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] = + useState(false) + const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] = + useState(false) const [, updateState] = useState({}) const removeItem = () => { @@ -672,10 +716,6 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { setShowUnsubscribeConfirmation(false) } - const [uploadingFiles, setUploadingFiles] = useState([]) - const [inDragOperation, setInDragOperation] = useState(false) - const [uploadProgress, setUploadProgress] = useState(0) - const handleDrop = async (acceptedFiles: any) => { setInDragOperation(false) setUploadingFiles(acceptedFiles.map((file: { name: any }) => file.name)) @@ -722,247 +762,215 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { props.reloadItems() } - const [showFilterMenu, setShowFilterMenu] = useState(false) - return ( - - { - console.log('searching with searchQuery: ', searchQuery) - props.applySearchQuery(searchQuery) + <> + - - { - console.log('searching with searchQuery: ', searchQuery) - props.applySearchQuery(searchQuery) + > + + + {props.isValidating && props.items.length == 0 && } + + { + setInDragOperation(true) }} - showFilterMenu={showFilterMenu} - setShowFilterMenu={setShowFilterMenu} - /> - { + setInDragOperation(false) + }} + preventDropOnDocument={true} + noClick={true} + accept={{ + 'application/pdf': ['.pdf'], }} > - - - {props.isValidating && props.items.length == 0 && } - - { - 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 && ( - - - - - - - - Uploading file - - - - - )} - - {!props.isValidating && props.items.length == 0 ? ( - { - props.setShowAddLinkModal(true) - }} - /> - ) : ( - - )} - - {props.hasMore ? ( - - ) : ( - - )} - -
- )} -
-
- {props.showAddLinkModal && ( - props.setShowAddLinkModal(false)} /> - )} - {props.showEditTitleModal && ( - - props.actionHandler('update-item', item) - } - onOpenChange={() => props.setShowEditTitleModal(false)} - item={props.linkToEdit as LibraryItem} - /> - )} - {showRemoveLinkConfirmation && ( - - - Are you sure you want to delete this item? All associated - notes and highlights will be deleted. - - {props.linkToRemove?.node && viewerData?.me && ( - + + + )} + {uploadingFiles.length > 0 && ( + + + + + + + + Uploading file + + + + + )} + + {!props.isValidating && props.items.length == 0 ? ( + { + props.setShowAddLinkModal(true) + }} + /> + ) : ( + + )} + + {props.hasMore ? ( + + ) : ( + )} -
+ + + )} + +
+ + {props.showAddLinkModal && ( + props.setShowAddLinkModal(false)} /> + )} + {props.showEditTitleModal && ( + + props.actionHandler('update-item', item) + } + onOpenChange={() => props.setShowEditTitleModal(false)} + item={props.linkToEdit as LibraryItem} + /> + )} + {showRemoveLinkConfirmation && ( + + + Are you sure you want to delete this item? All associated notes + and highlights will be deleted. + + {props.linkToRemove?.node && props.viewer && ( + + {}} + /> + + )} + + } + onAccept={removeItem} + acceptButtonLabel="Delete Item" + onOpenChange={() => setShowRemoveLinkConfirmation(false)} + /> + )} + {showUnsubscribeConfirmation && ( + setShowUnsubscribeConfirmation(false)} + /> + )} + {props.labelsTarget?.node.id && ( + { + if (props.labelsTarget) { + props.labelsTarget.node.labels = labels + updateState({}) } - 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) - } - }} - /> - )} - - + }} + 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 index 7a93ea756..28500cda4 100644 --- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -11,6 +11,7 @@ import { theme } from '../../tokens/stitches.config' import { currentThemeName } from '../../../lib/themeUpdater' import { MOBILE_HEADER_HEIGHT } from './HeaderSpacer' import { useRegisterActions } from 'kbar' +import { LibraryMode } from './HomeFeedContainer' export const LIBRARY_LEFT_MENU_WIDTH = '300px' @@ -22,6 +23,8 @@ type LibraryFilterMenuProps = { showFilterMenu: boolean setShowFilterMenu: (show: boolean) => void + + setMode: (mode: LibraryMode) => void } export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element { @@ -86,6 +89,7 @@ function SavedSearches(props: LibraryFilterMenuProps): JSX.Element { { name: 'Highlights', term: 'has:highlights', + mode: 'highlights', }, { name: 'Unlabeled', @@ -111,6 +115,7 @@ function SavedSearches(props: LibraryFilterMenuProps): JSX.Element { section: 'Saved Searches', keywords: '?' + item.name, perform: () => { + props.setMode(item.mode ?? 'reads') props.applySearchQuery(item.term) }, } @@ -125,6 +130,7 @@ function SavedSearches(props: LibraryFilterMenuProps): JSX.Element { key={item.name} text={item.name} filterTerm={item.term} + mode={item.mode} {...props} /> ))} @@ -285,8 +291,11 @@ type FilterButtonProps = { filterTerm: string searchTerm: string | undefined + + mode?: LibraryMode applySearchQuery: (searchTerm: string) => void + setMode: (mode: LibraryMode) => void setShowFilterMenu: (show: boolean) => void } @@ -330,6 +339,7 @@ function FilterButton(props: FilterButtonProps): JSX.Element { }, }} onClick={(e) => { + props.setMode(props.mode ?? 'reads') props.applySearchQuery(props.filterTerm) props.setShowFilterMenu(false) e.preventDefault() diff --git a/yarn.lock b/yarn.lock index 192472419..706119194 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5218,6 +5218,20 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.0" +"@radix-ui/react-switch@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.1.tgz#56665fa951a4190313be21fab70a90041bdbd191" + integrity sha512-tTxGluMtwrc5ffgAiOSMrYIx0r3vSTcgM4Vl8rqfpXcHt6ryB9B0OlFKUOiDpKASXlhvzfHf4Y0AYKJdpzjL8w== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-primitive" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.0" + "@radix-ui/react-use-previous" "1.0.0" + "@radix-ui/react-use-size" "1.0.0" + "@radix-ui/react-tooltip@^0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-0.1.7.tgz#6f8c00d6e489565d14abf209ce0fb8853c8c8ee3"