WIP: refactoring to reuse notebook code

This commit is contained in:
Jackson Harper 2023-03-27 12:16:21 +08:00
parent 53e5d9c6a0
commit 040e446167
7 changed files with 165 additions and 288 deletions

View file

@ -220,6 +220,7 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
width: '100%',
fontSize: '9px',
mt: '1px',
bg: 'red',
color: '$thTextSubtle',
}}
alignment="start"
@ -246,6 +247,17 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
)}`}
</>
) : null}
<SpanBox
css={{
width: '100%',
fontSize: '9px',
mt: '1px',
color: 'red',
marginLeft: 'auto',
}}
>
Save
</SpanBox>
</HStack>
</VStack>
) : (

View file

@ -28,7 +28,6 @@ const StyledQuote = styled(Blockquote, {
export function HighlightView(props: HighlightViewProps): JSX.Element {
const [noteMode, setNoteMode] = useState<'preview' | 'edit'>('preview')
const [isEditing, setIsEditing] = useState(false)
const lines = useMemo(
() => (props.highlight.quote || '').split('\n'),

View file

@ -1,4 +1,4 @@
import { Box, VStack, HStack } from '../../elements/LayoutPrimitives'
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { useCallback, useMemo, useState } from 'react'
import { CaretDown, CaretUp } from 'phosphor-react'
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
@ -7,9 +7,9 @@ import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { Button } from '../../elements/Button'
import { theme } from '../../tokens/stitches.config'
import { HighlightItem } from '../../templates/homeFeed/HighlightItem'
import { getHighlightLocation } from '../../templates/article/NotebookModal'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { HighlightView } from '../HighlightView'
export const GridSeparator = styled(Box, {
height: '1px',
@ -46,21 +46,23 @@ export function LibraryHighlightGridCard(
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)
return props.item.highlights
.filter((h) => h.type === 'HIGHLIGHT')
.sort((a: Highlight, b: Highlight) => {
if (a.highlightPositionPercent && b.highlightPositionPercent) {
return sorted(a.highlightPositionPercent, b.highlightPositionPercent)
}
} catch {}
return a.createdAt.localeCompare(b.createdAt)
})
// 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 (
@ -121,17 +123,20 @@ export function LibraryHighlightGridCard(
<>
<GridSeparator css={{ width: '100%' }} />
<VStack
css={{ height: '100%', width: '100%' }}
css={{ height: '100%', width: '100%', mt: '20px' }}
distribution="start"
>
{sortedHighlights.map((highlight) => (
<HighlightItem
key={highlight.id}
viewer={props.viewer}
item={props.item}
highlight={highlight}
deleteHighlight={props.deleteHighlight}
/>
<>
<HighlightView
key={highlight.id}
highlight={highlight}
updateHighlight={(highlight) => {
console.log('updated highlight: ', highlight)
}}
/>
<SpanBox css={{ mt: '10px', mb: '25px' }} />
</>
))}
</VStack>
</>

View file

@ -28,6 +28,7 @@ import { v4 as uuidv4 } from 'uuid'
import { nanoid } from 'nanoid'
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
import { HighlightNoteBox } from '../../patterns/HighlightNotes'
import { HighlightViewItem } from './HighlightViewItem'
type NotebookModalProps = {
pageId: string
@ -77,7 +78,6 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
deleteHighlightId?: string | undefined
}
) => {
console.log('annotationsReducer', action.type)
switch (action.type) {
case 'RESET': {
console.log(' -- reseting highlights: ', action.allHighlights)
@ -382,7 +382,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
<TitledSection title="HIGHLIGHTS" />
{sortedHighlights.map((highlight) => (
<ModalHighlightView
<HighlightViewItem
key={highlight.id}
highlight={highlight}
scrollToHighlight={props.scrollToHighlight}
@ -484,56 +484,6 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
)
}
type ModalHighlightViewProps = {
highlight: Highlight
scrollToHighlight?: (arg: string) => void
deleteHighlightAction: () => void
updateHighlight: (highlight: Highlight) => void
setSetLabelsTarget: (highlight: Highlight) => void
setShowConfirmDeleteHighlightId: (id: string | undefined) => void
}
function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element {
const [hover, setHover] = useState(false)
return (
<HStack
css={{ width: '100%', py: '20px', cursor: 'pointer' }}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<VStack css={{ width: '100%' }}>
<HighlightView
highlight={props.highlight}
scrollToHighlight={props.scrollToHighlight}
updateHighlight={props.updateHighlight}
/>
<SpanBox css={{ mb: '15px' }} />
</VStack>
<SpanBox
css={{
marginLeft: 'auto',
width: '20px',
visibility: hover ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<HighlightsMenu
highlight={props.highlight}
setLabelsTarget={props.setSetLabelsTarget}
setShowConfirmDeleteHighlightId={
props.setShowConfirmDeleteHighlightId
}
/>
</SpanBox>
</HStack>
)
}
type TitledSectionProps = {
title: string
editMode?: boolean

View file

@ -496,7 +496,11 @@ export default function PdfArticleContainer(
pageId={props.article.id}
highlights={highlightsRef.current}
onClose={(updatedHighlights, deletedAnnotations) => {
console.log('closed PDF notebook: ')
console.log(
'closed PDF notebook: ',
updatedHighlights,
deletedAnnotations
)
props.setShowHighlightsModal(false)
}}
/>

View file

@ -1,196 +1,11 @@
import { styled } from '@stitches/react'
import { useRouter } from 'next/router'
import { DotsThreeVertical } from 'phosphor-react'
import { Fragment, useCallback, useMemo, useState } from 'react'
import { useCallback } 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,
Box,
HStack,
SpanBox,
VStack,
} from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { Box } from '../../elements/LayoutPrimitives'
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, {
margin: '0px',
fontSize: '16px',
fontFamily: '$inter',
fontWeight: '500',
lineHeight: '1.50',
color: '$thHighContrast',
paddingLeft: '15px',
borderLeft: '2px solid $omnivoreCtaYellow',
})
export function HighlightItem(props: HighlightItemProps): JSX.Element {
const router = useRouter()
const [hover, setHover] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const lines = useMemo(
() => (props.highlight.quote || '').split('\n'),
[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"
>
<StyledQuote
onClick={(event) => {
if (router && props.viewer) {
const dest = `/${props.viewer.profile.username}/${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>
</StyledQuote>
<Box css={{ display: 'block' }}>
{props.highlight.labels?.map((label: Label, index: number) => (
<LabelChip
key={index}
text={label.name || ''}
color={label.color}
/>
))}
</Box>
{!isEditing && (
<StyledText
css={{
borderRadius: '6px',
bg: '#f6f8fa',
p: '10px',
width: '100%',
marginTop: '5px',
color: '#3D3D3D',
}}
onClick={() => setIsEditing(true)}
>
{props.highlight.annotation
? props.highlight.annotation
: 'Add notes to this highlight...'}
</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',
}}
>
<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')
props.deleteHighlight(props.item, props.highlight)
} 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
}}
/>
)}
</>
)
}
type HighlightsMenuProps = {
highlight: Highlight
@ -201,10 +16,15 @@ type HighlightsMenuProps = {
export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
const copyHighlight = useCallback(() => {
;(async () => {
await navigator.clipboard.writeText(props.highlight.quote)
showSuccessToast('Highlight copied')
})()
const quote = props.highlight.quote
if (quote) {
;(async () => {
await navigator.clipboard.writeText(quote)
showSuccessToast('Highlight copied')
})()
} else {
showErrorToast('No highlight text.')
}
}, [props.highlight])
return (

View file

@ -1,5 +1,5 @@
import { HighlighterCircle } from 'phosphor-react'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { Toaster } from 'react-hot-toast'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import {
@ -13,14 +13,17 @@ import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { MenuTrigger } from '../../elements/MenuTrigger'
import { StyledText } from '../../elements/StyledText'
import { HighlightNoteBox } from '../../patterns/HighlightNotes'
import { HighlightView } from '../../patterns/HighlightView'
import {
MetaStyle,
timeAgo,
} from '../../patterns/LibraryCards/LibraryCardStyles'
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
import { HighlightViewItem } from '../article/HighlightViewItem'
import { EmptyHighlights } from './EmptyHighlights'
import { HEADER_HEIGHT, MOBILE_HEADER_HEIGHT } from './HeaderSpacer'
import { HighlightItem, highlightsAsMarkdown } from './HighlightItem'
import { highlightsAsMarkdown } from './HighlightItem'
type HighlightItemsLayoutProps = {
items: LibraryItem[]
@ -165,7 +168,7 @@ export function HighlightItemsLayout(
borderBottom: '1px solid $thBorderColor',
}}
alignment="center"
distribution="start"
distribution="center"
></HStack>
<LibraryItemsList
items={items}
@ -366,6 +369,9 @@ type HighlightListProps = {
}
function HighlightList(props: HighlightListProps): JSX.Element {
const [notesEditMode, setNotesEditMode] = useState<'preview' | 'edit'>(
'preview'
)
const exportHighlights = useCallback(() => {
;(async () => {
if (!props.item.node.highlights) {
@ -378,6 +384,20 @@ function HighlightList(props: HighlightListProps): JSX.Element {
})()
}, [props.item.node.highlights])
const note = useMemo(() => {
const note = (props.item.node.highlights ?? []).find(
(h) => h.type === 'NOTE'
)
console.log('NOTE: ', note)
return note
}, [props.item])
const sortedHighlights = useMemo(() => {
return (props.item.node.highlights ?? []).filter(
(h) => h.type === 'HIGHLIGHT'
)
}, [props.item])
return (
<HStack
css={{
@ -402,8 +422,8 @@ function HighlightList(props: HighlightListProps): JSX.Element {
pt: '25px',
borderBottom: '1px solid $thBorderColor',
}}
alignment="start"
distribution="start"
alignment="center"
distribution="center"
>
<StyledText
css={{
@ -414,7 +434,7 @@ function HighlightList(props: HighlightListProps): JSX.Element {
color: 'thTextContrast2',
}}
>
HIGHLIGHTS
NOTEBOOK
</StyledText>
<Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
@ -425,18 +445,85 @@ function HighlightList(props: HighlightListProps): JSX.Element {
/>
</Dropdown>
</HStack>
<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>
<HStack
css={{
width: '100%',
pt: '25px',
borderBottom: '1px solid $thBorderColor',
}}
alignment="center"
distribution="center"
>
<StyledText
css={{
fontWeight: '600',
fontSize: '15px',
fontFamily: '$display',
width: '100%',
color: 'thTextContrast2',
}}
>
NOTE
</StyledText>
</HStack>
<HighlightNoteBox
sizeMode="normal"
mode={notesEditMode}
setEditMode={setNotesEditMode}
text={note?.annotation}
placeHolder="Add notes to this document..."
saveText={(highlight) => {
console.log('saving text', highlight)
}}
/>
<SpanBox css={{ mt: '10px', mb: '25px' }} />
{sortedHighlights && (
<>
<HStack
css={{
width: '100%',
pt: '25px',
borderBottom: '1px solid $thBorderColor',
}}
alignment="center"
distribution="center"
>
<StyledText
css={{
fontWeight: '600',
fontSize: '15px',
fontFamily: '$display',
width: '100%',
color: 'thTextContrast2',
}}
>
HIGHLIGHTS
</StyledText>
</HStack>
<VStack
css={{ width: '100%', mt: '20px' }}
distribution="start"
alignment="start"
>
{sortedHighlights.map((highlight) => (
<>
<HighlightViewItem
key={highlight.id}
highlight={highlight}
updateHighlight={(highlight) => {
console.log('updated highlight: ', highlight)
}}
/>
<SpanBox css={{ mt: '10px', mb: '25px' }} />
</>
))}
<Box css={{ height: '100px' }} />
</VStack>
<SpanBox css={{ mt: '10px', mb: '25px' }} />
</>
)}
</VStack>
</HStack>
)