Merge pull request #1923 from omnivore-app/fix/delete-highlight-fixes

Better handling of deleting highlights from the library highlights view
This commit is contained in:
Jackson Harper 2023-03-17 16:06:22 +08:00 committed by GitHub
commit b8b4fa888c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 224 additions and 78 deletions

View file

@ -1,5 +1,5 @@
import { Box, VStack, HStack } from '../../elements/LayoutPrimitives'
import { useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { CaretDown, CaretUp } from 'phosphor-react'
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
import { styled } from '@stitches/react'
@ -20,6 +20,8 @@ export const GridSeparator = styled(Box, {
type LibraryHighlightGridCardProps = {
viewer: UserBasicData
item: LibraryItemNode
deleteHighlight: (item: LibraryItemNode, highlight: Highlight) => void
}
export function LibraryHighlightGridCard(
@ -128,6 +130,7 @@ export function LibraryHighlightGridCard(
viewer={props.viewer}
item={props.item}
highlight={highlight}
deleteHighlight={props.deleteHighlight}
/>
))}
</VStack>

View file

@ -0,0 +1,23 @@
import { Book } from 'phosphor-react'
import { VStack } from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { theme } from '../../tokens/stitches.config'
export function EmptyHighlights(): JSX.Element {
return (
<VStack
alignment="center"
distribution="center"
css={{
color: '$grayTextContrast',
textAlign: 'center',
marginTop: '105px',
}}
>
<Book size={44} color={theme.colors.grayTextContrast.toString()} />
<StyledText style="fixedHeadline" css={{ color: '$grayTextContrast' }}>
No results found.
</StyledText>
</VStack>
)
}

View file

@ -18,6 +18,7 @@ export function EmptyLibrary(props: EmptyLibraryProps): JSX.Element {
color: '$grayTextContrast',
textAlign: 'center',
paddingTop: '88px',
flex: '1',
}}
>
<Book size={44} color={theme.colors.grayTextContrast.toString()} />

View file

@ -21,12 +21,15 @@ import {
} from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { theme } from '../../tokens/stitches.config'
import { SetLabelsModal } from '../article/SetLabelsModal'
type HighlightItemProps = {
highlight: Highlight
viewer: UserBasicData | undefined
item: LibraryItemNode
deleteHighlight: (item: LibraryItemNode, highlight: Highlight) => void
}
const StyledQuote = styled(Blockquote, {
@ -140,10 +143,6 @@ export function HighlightItem(props: HighlightItemProps): JSX.Element {
css={{
marginLeft: 'auto',
width: '20px',
visibility: hover ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<HighlightsMenu
@ -163,6 +162,7 @@ export function HighlightItem(props: HighlightItemProps): JSX.Element {
)
if (result) {
showSuccessToast('Highlight deleted')
props.deleteHighlight(props.item, props.highlight)
} else {
showErrorToast('Error deleting highlight')
}
@ -207,14 +207,6 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
})()
}, [props.highlight])
const exportHighlight = useCallback(() => {
;(async () => {
const markdown = highlightAsMarkdown(props.highlight)
await navigator.clipboard.writeText(markdown)
showSuccessToast('Highlight copied')
})()
}, [props.highlight])
return (
<Dropdown
triggerElement={
@ -231,7 +223,11 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
},
}}
>
<DotsThreeVertical size={20} color="#EBEBEB" weight="bold" />
<DotsThreeVertical
size={20}
color={theme.colors.thTextContrast2.toString()}
weight="bold"
/>
</Box>
}
>
@ -249,7 +245,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
/>
<DropdownOption
onSelect={() => {
exportHighlight()
props.setShowConfirmDeleteHighlightId(props.highlight.id)
}}
title="Delete"
/>

View file

@ -1,7 +1,11 @@
import { HighlighterCircle } from 'phosphor-react'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { Toaster } from 'react-hot-toast'
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import {
LibraryItem,
LibraryItemNode,
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
@ -14,6 +18,8 @@ import {
timeAgo,
} from '../../patterns/LibraryCards/LibraryCardStyles'
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
import { EmptyHighlights } from './EmptyHighlights'
import { HEADER_HEIGHT, MOBILE_HEADER_HEIGHT } from './HeaderSpacer'
import { HighlightItem, highlightsAsMarkdown } from './HighlightItem'
type HighlightItemsLayoutProps = {
@ -30,99 +36,152 @@ export function HighlightItemsLayout(
undefined
)
const listReducer = (
state: LibraryItem[],
action: {
type: string
itemId?: string
highlightId?: string
items?: LibraryItem[]
}
) => {
switch (action.type) {
case 'RESET':
return action.items ?? []
case 'REMOVE_HIGHLIGHT':
const item = state.find((li) => li.node.id === action.itemId)
if (item && item.node.highlights) {
item.node.highlights = item.node.highlights.filter(
(h) => h.id !== action.highlightId
)
}
const result = state.filter(
(item) => item.node.highlights && item.node.highlights.length > 0
)
if (
item &&
item == currentItem &&
(item.node.highlights?.length ?? 0) < 1
) {
setCurrentItem(result.length > 0 ? result[0] : undefined)
}
return result
default:
throw new Error()
}
}
const [items, dispatchList] = useReducer(listReducer, [])
function handleDelete(item: LibraryItemNode, highlight: Highlight) {
dispatchList({
type: 'REMOVE_HIGHLIGHT',
itemId: item.id,
highlightId: highlight.id,
})
}
useEffect(() => {
dispatchList({
type: 'RESET',
items: props.items,
})
}, [props.items])
useEffect(() => {
// Only set the current item on larger screens
if (window.innerWidth >= 992 /* lgDown */) {
if (!currentItem && props.items.length > 0) {
setCurrentItem(props.items[0])
if (!currentItem && items.length > 0) {
setCurrentItem(items[0])
}
}
}, [currentItem, setCurrentItem, props.items])
}, [currentItem, setCurrentItem, items])
if (items.length < 1) {
return (
<Box
css={{
width: '100%',
height: `calc(100vh - ${HEADER_HEIGHT})`,
'@xlgDown': {
height: `calc(100vh - ${MOBILE_HEADER_HEIGHT})`,
},
}}
>
<EmptyHighlights />
</Box>
)
}
return (
<>
<HStack
css={{
width: '100%',
height: '100%',
height: `calc(100vh - ${HEADER_HEIGHT})`,
'@xlgDown': {
height: `calc(100vh - ${MOBILE_HEADER_HEIGHT})`,
},
bg: '$thBackground2',
overflow: 'hidden',
}}
distribution="start"
alignment="start"
>
<Toaster />
<VStack
<HStack
css={{
flexGrow: '0',
width: '430px',
minWidth: '430px',
overflowY: 'scroll',
height: '100%',
bg: '$thBackground',
'@lgDown': {
width: '100%',
minWidth: 'unset',
},
}}
distribution="start"
alignment="start"
>
<HStack
<VStack
css={{
width: 'calc(100% - 35px)',
height: '55px',
mx: '20px',
borderBottom: '1px solid $thBorderColor',
minHeight: `calc(100vh - ${HEADER_HEIGHT})`,
'@xlgDown': {
minHeight: `calc(100vh - ${MOBILE_HEADER_HEIGHT})`,
},
bg: '$thBackground',
}}
alignment="center"
distribution="start"
alignment="start"
>
{/* <Box
<HStack
css={{
display: 'flex',
height: '20px',
width: '20px',
marginLeft: 'auto',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '1000px',
'&:hover': {
bg: '#898989',
},
width: 'calc(100% - 35px)',
height: '55px',
mx: '20px',
borderBottom: '1px solid $thBorderColor',
}}
>
<DotsThreeVertical size={200} color="#898989" weight="bold" />
</Box> */}
</HStack>
{props.items.map((linkedItem) => (
<Box
className="linkedItemCard"
data-testid="linkedItemCard"
id={linkedItem.node.id}
tabIndex={0}
key={linkedItem.node.id}
css={{
width: '100%',
height: '100%',
px: '15px',
}}
onClick={(event) => {
setCurrentItem(linkedItem)
event.preventDefault()
}}
>
{props.viewer && (
<LibraryItemCard
item={linkedItem}
viewer={props.viewer}
selected={currentItem?.node.id == linkedItem.node.id}
/>
)}
</Box>
))}
</VStack>
alignment="center"
distribution="start"
></HStack>
<LibraryItemsList
items={items}
viewer={props.viewer}
currentItem={currentItem}
setCurrentItem={setCurrentItem}
deleteHighlight={handleDelete}
/>
<Box css={{ height: '100px' }} />
</VStack>
</HStack>
{currentItem && (
<>
<SpanBox
css={{
display: 'flex',
height: '100%',
width: '100%',
flexGrow: '1',
'@lgDown': {
display: 'none',
@ -130,7 +189,22 @@ export function HighlightItemsLayout(
},
}}
>
<HighlightList item={currentItem} viewer={props.viewer} />
<HStack
css={{
flexGrow: '1',
overflowY: 'scroll',
height: '100%',
width: '100%',
}}
distribution="start"
alignment="start"
>
<HighlightList
item={currentItem}
viewer={props.viewer}
deleteHighlight={handleDelete}
/>
</HStack>
</SpanBox>
</>
)}
@ -139,10 +213,56 @@ export function HighlightItemsLayout(
)
}
type LibraryItemsListProps = {
items: LibraryItem[]
viewer: UserBasicData | undefined
currentItem: LibraryItem | undefined
setCurrentItem: (item: LibraryItem | undefined) => void
deleteHighlight: (item: LibraryItemNode, highlight: Highlight) => void
}
function LibraryItemsList(props: LibraryItemsListProps): JSX.Element {
return (
<>
{props.items.map((linkedItem) => (
<Box
className="linkedItemCard"
data-testid="linkedItemCard"
id={linkedItem.node.id}
tabIndex={0}
key={linkedItem.node.id}
css={{
width: '100%',
height: '100%',
px: '15px',
cursor: 'pointer',
}}
onClick={(event) => {
props.setCurrentItem(linkedItem)
event.preventDefault()
}}
>
{props.viewer && (
<LibraryItemCard
item={linkedItem}
viewer={props.viewer}
selected={props.currentItem?.node.id == linkedItem.node.id}
deleteHighlight={props.deleteHighlight}
/>
)}
</Box>
))}
</>
)
}
type HighlightTitleCardProps = {
item: LibraryItem
viewer: UserBasicData
selected: boolean
deleteHighlight: (item: LibraryItemNode, highlight: Highlight) => void
}
function LibraryItemCard(props: HighlightTitleCardProps): JSX.Element {
@ -152,6 +272,7 @@ function LibraryItemCard(props: HighlightTitleCardProps): JSX.Element {
<LibraryHighlightGridCard
item={props.item.node}
viewer={props.viewer}
deleteHighlight={props.deleteHighlight}
/>
</SpanBox>
<SpanBox css={{ display: 'none', '@lg': { display: 'flex' } }}>
@ -238,6 +359,8 @@ function HighlightTitleCard(props: HighlightTitleCardProps): JSX.Element {
type HighlightListProps = {
item: LibraryItem
viewer: UserBasicData | undefined
deleteHighlight: (item: LibraryItemNode, highlight: Highlight) => void
}
function HighlightList(props: HighlightListProps): JSX.Element {
@ -266,7 +389,6 @@ function HighlightList(props: HighlightListProps): JSX.Element {
<VStack
css={{
width: '425px',
height: '100%',
borderRadius: '6px',
}}
alignment="start"
@ -275,11 +397,10 @@ function HighlightList(props: HighlightListProps): JSX.Element {
<HStack
css={{
width: '100%',
height: '100%',
pt: '25px',
borderBottom: '1px solid $thBorderColor',
}}
alignment="center"
alignment="start"
distribution="start"
>
<StyledText
@ -288,7 +409,7 @@ function HighlightList(props: HighlightListProps): JSX.Element {
fontSize: '15px',
fontFamily: '$display',
width: '100%',
color: '$thTextContrast2',
color: 'thTextContrast2',
}}
>
HIGHLIGHTS
@ -302,15 +423,17 @@ function HighlightList(props: HighlightListProps): JSX.Element {
/>
</Dropdown>
</HStack>
<VStack css={{ height: '100%', width: '100%' }} distribution="start">
<VStack css={{ width: '100%' }} distribution="start" alignment="start">
{(props.item.node.highlights ?? []).map((highlight) => (
<HighlightItem
key={highlight.id}
viewer={props.viewer}
item={props.item.node}
highlight={highlight}
deleteHighlight={props.deleteHighlight}
/>
))}
<Box css={{ height: '100px' }} />
</VStack>
</VStack>
</HStack>

View file

@ -686,7 +686,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
setShowFilterMenu={setShowFilterMenu}
/>
{props.mode == 'highlights' && (
{!props.isValidating && props.mode == 'highlights' && (
<HighlightItemsLayout
gridContainerRef={props.gridContainerRef}
items={props.items}