mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Sort highlights, make the notes editable
This commit is contained in:
parent
e9b4cdd7bf
commit
1067726a7a
4 changed files with 199 additions and 174 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { Box, VStack, HStack } from '../../elements/LayoutPrimitives'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { CaretDown, CaretUp } from 'phosphor-react'
|
||||
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
|
||||
import { styled } from '@stitches/react'
|
||||
|
|
@ -8,6 +8,7 @@ import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryIt
|
|||
import { Button } from '../../elements/Button'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { HighlightItem } from '../../templates/homeFeed/HighlightItem'
|
||||
import { getHighlightLocation } from '../../templates/article/NotebookModal'
|
||||
|
||||
export const GridSeparator = styled(Box, {
|
||||
height: '1px',
|
||||
|
|
@ -23,11 +24,42 @@ type LibraryHighlightGridCardProps = {
|
|||
export function LibraryHighlightGridCard(
|
||||
props: LibraryHighlightGridCardProps
|
||||
): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const higlightCount = props.item.highlights?.length ?? 0
|
||||
|
||||
const sortedHighlights = useMemo(() => {
|
||||
const sorted = (a: number, b: number) => {
|
||||
if (a < b) {
|
||||
return -1
|
||||
}
|
||||
if (a > b) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (!props.item.highlights) {
|
||||
return []
|
||||
}
|
||||
|
||||
return props.item.highlights.sort((a: Highlight, b: Highlight) => {
|
||||
if (a.highlightPositionPercent && b.highlightPositionPercent) {
|
||||
return sorted(a.highlightPositionPercent, b.highlightPositionPercent)
|
||||
}
|
||||
// We do this in a try/catch because it might be an invalid diff
|
||||
// With PDF it will definitely be an invalid diff.
|
||||
try {
|
||||
const aPos = getHighlightLocation(a.patch)
|
||||
const bPos = getHighlightLocation(b.patch)
|
||||
if (aPos && bPos) {
|
||||
return sorted(aPos, bPos)
|
||||
}
|
||||
} catch {}
|
||||
return a.createdAt.localeCompare(b.createdAt)
|
||||
})
|
||||
}, [props.item.highlights])
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
|
|
@ -46,12 +78,6 @@ export function LibraryHighlightGridCard(
|
|||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
{!expanded && (
|
||||
<HStack
|
||||
|
|
@ -95,7 +121,7 @@ export function LibraryHighlightGridCard(
|
|||
css={{ height: '100%', width: '100%' }}
|
||||
distribution="start"
|
||||
>
|
||||
{(props.item.highlights ?? []).map((highlight) => (
|
||||
{sortedHighlights.map((highlight) => (
|
||||
<HighlightItem
|
||||
key={highlight.id}
|
||||
viewer={props.viewer}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabe
|
|||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { diff_match_patch } from 'diff-match-patch'
|
||||
import { HighlightNoteTextEditArea } from '../../elements/HighlightNoteTextEditArea'
|
||||
|
||||
type NotebookModalProps = {
|
||||
highlights: Highlight[]
|
||||
|
|
@ -224,7 +225,7 @@ function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element {
|
|||
</StyledText>
|
||||
) : null}
|
||||
{isEditing && (
|
||||
<TextEditArea
|
||||
<HighlightNoteTextEditArea
|
||||
setIsEditing={setIsEditing}
|
||||
highlight={props.highlight}
|
||||
updateHighlight={props.updateHighlight}
|
||||
|
|
@ -235,86 +236,3 @@ function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type TextEditAreaProps = {
|
||||
setIsEditing: (editing: boolean) => void
|
||||
highlight: Highlight
|
||||
updateHighlight: (highlight: Highlight) => void
|
||||
}
|
||||
|
||||
export const TextEditArea = (props: TextEditAreaProps): JSX.Element => {
|
||||
const [noteContent, setNoteContent] = useState(
|
||||
props.highlight.annotation ?? ''
|
||||
)
|
||||
|
||||
const handleNoteContentChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
setNoteContent(event.target.value)
|
||||
},
|
||||
[setNoteContent]
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%' }} key="textEditor">
|
||||
<StyledTextArea
|
||||
css={{
|
||||
my: '$3',
|
||||
minHeight: '$6',
|
||||
borderRadius: '6px',
|
||||
bg: '$grayBase',
|
||||
p: '16px',
|
||||
width: '100%',
|
||||
marginTop: '16px',
|
||||
resize: 'vertical',
|
||||
}}
|
||||
autoFocus
|
||||
maxLength={4000}
|
||||
value={noteContent}
|
||||
placeholder={'Add your notes...'}
|
||||
onChange={handleNoteContentChange}
|
||||
/>
|
||||
<HStack alignment="center" distribution="end" css={{ width: '100%' }}>
|
||||
<Button
|
||||
style="ctaPill"
|
||||
css={{ mr: '$2' }}
|
||||
onClick={() => {
|
||||
props.setIsEditing(false)
|
||||
setNoteContent(props.highlight.annotation ?? '')
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
console.log('updating highlight')
|
||||
try {
|
||||
const result = await updateHighlightMutation({
|
||||
highlightId: props.highlight.id,
|
||||
annotation: noteContent,
|
||||
})
|
||||
console.log('result: ' + result)
|
||||
|
||||
if (!result) {
|
||||
showErrorToast('There was an error updating your highlight.')
|
||||
} else {
|
||||
showSuccessToast('Note saved')
|
||||
props.highlight.annotation = noteContent
|
||||
props.updateHighlight(props.highlight)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error updating annoation', err)
|
||||
showErrorToast('There was an error updating your highlight.')
|
||||
}
|
||||
|
||||
props.setIsEditing(false)
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DotsThreeVertical } from 'phosphor-react'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import { Fragment, useCallback, useMemo, useState } from 'react'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight'
|
||||
import { 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'
|
||||
import { HighlightNoteTextEditArea } from '../../elements/HighlightNoteTextEditArea'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import {
|
||||
Blockquote,
|
||||
|
|
@ -16,6 +20,8 @@ import {
|
|||
VStack,
|
||||
} from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
|
||||
import { SetLabelsModal } from '../article/SetLabelsModal'
|
||||
|
||||
type HighlightItemProps = {
|
||||
highlight: Highlight
|
||||
|
|
@ -44,89 +50,162 @@ export function HighlightItem(props: HighlightItemProps): JSX.Element {
|
|||
[props.highlight.quote]
|
||||
)
|
||||
|
||||
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
|
||||
useState<undefined | string>(undefined)
|
||||
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [, updateState] = useState({})
|
||||
|
||||
return (
|
||||
<HStack
|
||||
css={{ width: '100%', py: '20px', cursor: 'pointer' }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'clip',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onClick={(event) => {
|
||||
if (router && props.viewer) {
|
||||
const dest = `/${props.viewer}/${props.item.slug}#${props.highlight.id}`
|
||||
router.push(dest)
|
||||
}
|
||||
event.preventDefault()
|
||||
}}
|
||||
<>
|
||||
<HStack
|
||||
css={{ width: '100%', py: '20px', cursor: 'pointer' }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<StyledQuote>
|
||||
<SpanBox css={{ p: '1px', borderRadius: '2px' }}>
|
||||
{lines.map((line: string, index: number) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
{index !== lines.length - 1 && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</SpanBox>
|
||||
<Box css={{ display: 'block', pt: '16px' }}>
|
||||
{props.highlight.labels?.map((label: Label, index: number) => (
|
||||
<LabelChip
|
||||
key={index}
|
||||
text={label.name || ''}
|
||||
color={label.color}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</StyledQuote>
|
||||
|
||||
<StyledText
|
||||
<VStack
|
||||
css={{
|
||||
borderRadius: '6px',
|
||||
bg: '#EBEBEB',
|
||||
p: '10px',
|
||||
gap: '10px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
marginTop: '5px',
|
||||
color: '#3D3D3D',
|
||||
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'clip',
|
||||
}}
|
||||
onClick={() => setIsEditing(true)}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
{props.highlight.annotation
|
||||
? props.highlight.annotation
|
||||
: 'Add your notes...'}
|
||||
</StyledText>
|
||||
</VStack>
|
||||
<SpanBox
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
width: '20px',
|
||||
visibility: hover ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HighlightsMenu />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
<StyledQuote
|
||||
onClick={(event) => {
|
||||
if (router && props.viewer) {
|
||||
const dest = `/${props.viewer}/${props.item.slug}#${props.highlight.id}`
|
||||
router.push(dest)
|
||||
}
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<SpanBox css={{ p: '1px', borderRadius: '2px' }}>
|
||||
{lines.map((line: string, index: number) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
{index !== lines.length - 1 && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</SpanBox>
|
||||
<Box css={{ display: 'block', pt: '16px' }}>
|
||||
{props.highlight.labels?.map((label: Label, index: number) => (
|
||||
<LabelChip
|
||||
key={index}
|
||||
text={label.name || ''}
|
||||
color={label.color}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</StyledQuote>
|
||||
|
||||
{!isEditing && (
|
||||
<StyledText
|
||||
css={{
|
||||
borderRadius: '6px',
|
||||
bg: '#EBEBEB',
|
||||
p: '10px',
|
||||
width: '100%',
|
||||
marginTop: '5px',
|
||||
color: '#3D3D3D',
|
||||
}}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{props.highlight.annotation
|
||||
? props.highlight.annotation
|
||||
: 'Add your notes...'}
|
||||
</StyledText>
|
||||
)}
|
||||
{isEditing && (
|
||||
<HighlightNoteTextEditArea
|
||||
setIsEditing={setIsEditing}
|
||||
highlight={props.highlight}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
updateHighlight={() => {}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
<SpanBox
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
width: '20px',
|
||||
visibility: hover ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HighlightsMenu
|
||||
highlight={props.highlight}
|
||||
setLabelsTarget={setLabelsTarget}
|
||||
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
|
||||
/>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
{showConfirmDeleteHighlightId && (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure you want to delete this highlight?'}
|
||||
onAccept={async () => {
|
||||
setShowConfirmDeleteHighlightId(undefined)
|
||||
const result = await deleteHighlightMutation(
|
||||
showConfirmDeleteHighlightId
|
||||
)
|
||||
if (result) {
|
||||
showSuccessToast('Highlight deleted')
|
||||
} else {
|
||||
showErrorToast('Error deleting highlight')
|
||||
}
|
||||
}}
|
||||
onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)}
|
||||
/>
|
||||
)}
|
||||
{labelsTarget && (
|
||||
<SetLabelsModal
|
||||
provider={labelsTarget}
|
||||
onOpenChange={function (open: boolean): void {
|
||||
setLabelsTarget(undefined)
|
||||
}}
|
||||
onLabelsUpdated={function (labels: Label[]): void {
|
||||
updateState({})
|
||||
}}
|
||||
save={function (labels: Label[]): Promise<Label[] | undefined> {
|
||||
const result = setLabelsForHighlight(
|
||||
labelsTarget.id,
|
||||
labels.map((label) => label.id)
|
||||
)
|
||||
return result
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function HighlightsMenu(): JSX.Element {
|
||||
type HighlightsMenuProps = {
|
||||
highlight: Highlight
|
||||
|
||||
setLabelsTarget: (target: Highlight) => void
|
||||
setShowConfirmDeleteHighlightId: (set: string) => void
|
||||
}
|
||||
|
||||
function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
|
||||
const copyHighlight = useCallback(() => {
|
||||
;(async () => {
|
||||
await navigator.clipboard.writeText(props.highlight.quote)
|
||||
showSuccessToast('Highlight copied')
|
||||
})()
|
||||
}, [props.highlight])
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
triggerElement={
|
||||
|
|
@ -148,23 +227,23 @@ function HighlightsMenu(): JSX.Element {
|
|||
}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
console.log('copy')
|
||||
onSelect={async () => {
|
||||
copyHighlight()
|
||||
}}
|
||||
title="Copy"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
console.log('labels')
|
||||
props.setLabelsTarget(props.highlight)
|
||||
}}
|
||||
title="Labels"
|
||||
/>
|
||||
<DropdownOption
|
||||
{/* <DropdownOption
|
||||
onSelect={() => {
|
||||
console.log('delete')
|
||||
props.setShowConfirmDeleteHighlightId(props.highlight.id)
|
||||
}}
|
||||
title="Delete"
|
||||
/>
|
||||
/> */}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { DotsThreeVertical, HighlighterCircle } from 'phosphor-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
|
||||
|
|
@ -46,6 +47,7 @@ export function HighlightItemsLayout(
|
|||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
<Toaster />
|
||||
<VStack
|
||||
css={{
|
||||
width: '430px',
|
||||
|
|
|
|||
Loading…
Reference in a new issue