Sort highlights, make the notes editable

This commit is contained in:
Jackson Harper 2023-03-08 17:48:30 +08:00
parent e9b4cdd7bf
commit 1067726a7a
4 changed files with 199 additions and 174 deletions

View file

@ -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}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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',