mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add new highlights layout
This commit is contained in:
parent
4b4e1a9a52
commit
da03ad9bf3
6 changed files with 696 additions and 254 deletions
20
packages/web/components/elements/images/SideBarIcon.tsx
Normal file
20
packages/web/components/elements/images/SideBarIcon.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
type SideBarProps = {
|
||||
strokeColor: string
|
||||
}
|
||||
|
||||
export function SideBarIcon(props: SideBarProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="19"
|
||||
height="14"
|
||||
viewBox="0 0 19 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.10293 12.2383H7.3014V1.08129H6.10293V12.2383ZM4.62964 3.9129C4.74669 3.9129 4.84847 3.87289 4.93499 3.79286C5.0215 3.70812 5.06476 3.61633 5.06476 3.51747C5.06476 3.40919 5.0215 3.31739 4.93499 3.24207C4.84847 3.16204 4.74669 3.12203 4.62964 3.12203H2.99606C2.87901 3.12203 2.77723 3.16204 2.69071 3.24207C2.6042 3.31739 2.56094 3.40919 2.56094 3.51747C2.56094 3.61633 2.6042 3.70812 2.69071 3.79286C2.77723 3.87289 2.87901 3.9129 2.99606 3.9129H4.62964ZM4.62964 5.7418C4.74669 5.7418 4.84847 5.70179 4.93499 5.62176C5.0215 5.54173 5.06476 5.44758 5.06476 5.3393C5.06476 5.23574 5.0215 5.14629 4.93499 5.07097C4.84847 4.99094 4.74669 4.95093 4.62964 4.95093H2.99606C2.87901 4.95093 2.77723 4.99094 2.69071 5.07097C2.6042 5.14629 2.56094 5.23574 2.56094 5.3393C2.56094 5.44758 2.6042 5.54173 2.69071 5.62176C2.77723 5.70179 2.87901 5.7418 2.99606 5.7418H4.62964ZM4.62964 7.56364C4.74669 7.56364 4.84847 7.52598 4.93499 7.45066C5.0215 7.37063 5.06476 7.27883 5.06476 7.17526C5.06476 7.06699 5.0215 6.97519 4.93499 6.89987C4.84847 6.81984 4.74669 6.77982 4.62964 6.77982H2.99606C2.87901 6.77982 2.77723 6.81984 2.69071 6.89987C2.6042 6.97519 2.56094 7.06699 2.56094 7.17526C2.56094 7.27883 2.6042 7.37063 2.69071 7.45066C2.77723 7.52598 2.87901 7.56364 2.99606 7.56364H4.62964ZM2.68308 13.1562H15.8892C16.6933 13.1562 17.2938 12.9727 17.6907 12.6055C18.0877 12.243 18.2861 11.6969 18.2861 10.9672V2.34528C18.2861 1.6156 18.0877 1.06952 17.6907 0.707038C17.2938 0.339846 16.6933 0.15625 15.8892 0.15625H2.68308C1.8841 0.15625 1.28359 0.339846 0.881553 0.707038C0.484606 1.06952 0.286133 1.6156 0.286133 2.34528V10.9672C0.286133 11.6969 0.484606 12.243 0.881553 12.6055C1.28359 12.9727 1.8841 13.1562 2.68308 13.1562ZM2.69835 12.0194C2.31667 12.0194 2.02405 11.9276 1.82048 11.744C1.61692 11.5557 1.51514 11.2779 1.51514 10.9107V2.40177C1.51514 2.03458 1.61692 1.75918 1.82048 1.57559C2.02405 1.38728 2.31667 1.29313 2.69835 1.29313H15.8739C16.2505 1.29313 16.5406 1.38728 16.7441 1.57559C16.9528 1.75918 17.0571 2.03458 17.0571 2.40177V10.9107C17.0571 11.2779 16.9528 11.5557 16.7441 11.744C16.5406 11.9276 16.2505 12.0194 15.8739 12.0194H2.69835Z"
|
||||
fill="#898989"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 ?? ''
|
||||
)
|
||||
|
|
|
|||
390
packages/web/components/templates/homeFeed/HighlightsLayout.tsx
Normal file
390
packages/web/components/templates/homeFeed/HighlightsLayout.tsx
Normal file
|
|
@ -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<HTMLDivElement>
|
||||
}
|
||||
|
||||
export function HighlightItemsLayout(
|
||||
props: HighlightItemsLayoutProps
|
||||
): JSX.Element {
|
||||
const [currentItem, setCurrentItem] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentItem && props.items.length > 0) {
|
||||
setCurrentItem(props.items[0])
|
||||
}
|
||||
}, [currentItem, setCurrentItem, props.items])
|
||||
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bg: '#333333',
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
width: '430px',
|
||||
height: '100%',
|
||||
bg: '#2A2A2A',
|
||||
overflowY: 'scroll',
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: 'calc(100% - 35px)',
|
||||
height: '55px',
|
||||
mx: '20px',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
>
|
||||
<SideBarIcon strokeColor={theme.colors.thTextContrast.toString()} />
|
||||
</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 && (
|
||||
<HighlightTitleCard
|
||||
item={linkedItem}
|
||||
viewer={props.viewer}
|
||||
selected={currentItem?.node.id == linkedItem.node.id}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
{currentItem && (
|
||||
<HighlightItemsCard item={currentItem} viewer={props.viewer} />
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<HStack
|
||||
css={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
py: '10px',
|
||||
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
}}
|
||||
distribution="start"
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
py: '15px',
|
||||
px: '15px',
|
||||
borderRadius: '10px',
|
||||
bg: props.selected ? '#3D3D3D' : 'unset',
|
||||
}}
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<HStack css={MetaStyle} distribution="start">
|
||||
<Box>
|
||||
{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 ? (
|
||||
<>
|
||||
{` • `}
|
||||
<SpanBox css={{ color: '#55B938' }}>
|
||||
{`${Math.round(props.item.node.readingProgressPercent)}%`}
|
||||
</SpanBox>
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box
|
||||
css={{
|
||||
mt: '5px',
|
||||
color: '$thTextContrast2',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.50',
|
||||
fontFamily: '$display',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{props.item.node.title}
|
||||
</Box>
|
||||
</VStack>
|
||||
|
||||
<HighlightCountChip
|
||||
selected={props.selected}
|
||||
count={props.item.node.highlights?.length ?? 0}
|
||||
/>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightCountChipProps = {
|
||||
count: number
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
function HighlightCountChip(props: HighlightCountChipProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
gap: '5px',
|
||||
borderRadius: '12px',
|
||||
marginLeft: 'auto',
|
||||
minWidth: '50px',
|
||||
minHeight: '25px',
|
||||
width: '47px',
|
||||
color: props.selected ? '#3D3D3D' : '#898989',
|
||||
bg: props.selected ? '#D9D9D9' : '#3D3D3D',
|
||||
fontFamily: '$inter',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
ml: '30px',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
>
|
||||
{props.count}
|
||||
<HighlighterCircle
|
||||
size={15}
|
||||
color={props.selected ? '#3D3D3D' : '#898989'}
|
||||
/>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightItemsCardProps = {
|
||||
item: LibraryItem
|
||||
viewer: UserBasicData | undefined
|
||||
}
|
||||
|
||||
function HighlightItemsCard(props: HighlightItemsCardProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
css={{ height: '100%', flexGrow: '1' }}
|
||||
distribution="center"
|
||||
alignment="start"
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
width: '425px',
|
||||
height: '100%',
|
||||
// mt: '20px',
|
||||
//border: '1px solid $thBorderColor',
|
||||
borderRadius: '6px',
|
||||
// bg: '#2A2A2A',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pt: '25px',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
fontFamily: '$display',
|
||||
}}
|
||||
>
|
||||
HIGHLIGHTS
|
||||
</StyledText>
|
||||
</HStack>
|
||||
<VStack css={{ height: '100%', width: '100%' }} distribution="start">
|
||||
{(props.item.node.highlights ?? []).map((highlight) => (
|
||||
<HighlightItemCard key={highlight.id} highlight={highlight} />
|
||||
))}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<HStack
|
||||
css={{ width: '100%', py: '20px' }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
height: '100%',
|
||||
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'clip',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<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>
|
||||
|
||||
{!isEditing ? (
|
||||
<StyledText
|
||||
css={{
|
||||
borderRadius: '6px',
|
||||
bg: '#3D3D3D',
|
||||
p: '10px',
|
||||
width: '100%',
|
||||
marginTop: '5px',
|
||||
color: '$grayText',
|
||||
}}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{props.highlight.annotation
|
||||
? props.highlight.annotation
|
||||
: 'Add your notes...'}
|
||||
</StyledText>
|
||||
) : null}
|
||||
{isEditing && (
|
||||
<TextEditArea
|
||||
setIsEditing={setIsEditing}
|
||||
highlight={props.highlight}
|
||||
updateHighlight={() => {}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
<SpanBox
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
width: '20px',
|
||||
visibility: hover ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DotsThreeVertical size={20} color="#EBEBEB" weight="bold" />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string, string> = {
|
||||
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<LibraryMode>('reads')
|
||||
const [layout, setLayout] = usePersistedState<LayoutType>({
|
||||
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 (
|
||||
<VStack css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryHeader
|
||||
layout={layout}
|
||||
updateLayout={updateLayout}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
console.log('searching with searchQuery: ', searchQuery)
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
<HStack css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryFilterMenu
|
||||
setShowAddLinkModal={props.setShowAddLinkModal}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
console.log('searching with searchQuery: ', searchQuery)
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
setMode={setMode}
|
||||
/>
|
||||
|
||||
{mode == 'highlights' && (
|
||||
<HighlightItemsLayout
|
||||
gridContainerRef={props.gridContainerRef}
|
||||
items={props.items}
|
||||
viewer={viewerData?.me}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode == 'reads' && (
|
||||
<LibraryItemsLayout
|
||||
viewer={viewerData?.me}
|
||||
layout={layout}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<VStack css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryHeader
|
||||
layout={layout}
|
||||
updateLayout={updateLayout}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
console.log('searching with searchQuery: ', searchQuery)
|
||||
props.applySearchQuery(searchQuery)
|
||||
<>
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{
|
||||
height: '100%',
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
<HStack css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryFilterMenu
|
||||
setShowAddLinkModal={props.setShowAddLinkModal}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
console.log('searching with searchQuery: ', searchQuery)
|
||||
props.applySearchQuery(searchQuery)
|
||||
>
|
||||
<Toaster />
|
||||
|
||||
{props.isValidating && props.items.length == 0 && <TopBarProgress />}
|
||||
|
||||
<Dropzone
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={() => {
|
||||
setInDragOperation(true)
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '100%',
|
||||
onDragLeave={() => {
|
||||
setInDragOperation(false)
|
||||
}}
|
||||
preventDropOnDocument={true}
|
||||
noClick={true}
|
||||
accept={{
|
||||
'application/pdf': ['.pdf'],
|
||||
}}
|
||||
>
|
||||
<Toaster />
|
||||
|
||||
{props.isValidating && props.items.length == 0 && <TopBarProgress />}
|
||||
|
||||
<Dropzone
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={() => {
|
||||
setInDragOperation(true)
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setInDragOperation(false)
|
||||
}}
|
||||
preventDropOnDocument={true}
|
||||
noClick={true}
|
||||
accept={{
|
||||
'application/pdf': ['.pdf'],
|
||||
}}
|
||||
>
|
||||
{({
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
acceptedFiles,
|
||||
fileRejections,
|
||||
}) => (
|
||||
<div
|
||||
{...getRootProps({ className: 'dropzone' })}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
{inDragOperation && uploadingFiles.length < 1 && (
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<Box
|
||||
css={{
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
}}
|
||||
>
|
||||
Drop PDF document to to upload and add to your library
|
||||
</Box>
|
||||
</DragnDropStyle>
|
||||
</DragnDropContainer>
|
||||
)}
|
||||
{uploadingFiles.length > 0 && (
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<Box
|
||||
css={{
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
width: '80%',
|
||||
}}
|
||||
>
|
||||
<Progress.Root
|
||||
className="ProgressRoot"
|
||||
value={uploadProgress}
|
||||
>
|
||||
<Progress.Indicator
|
||||
className="ProgressIndicator"
|
||||
style={{
|
||||
transform: `translateX(-${
|
||||
100 - uploadProgress
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</Progress.Root>
|
||||
<StyledText
|
||||
style="boldText"
|
||||
css={{
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
Uploading file
|
||||
</StyledText>
|
||||
</Box>
|
||||
</DragnDropStyle>
|
||||
</DragnDropContainer>
|
||||
)}
|
||||
<input {...getInputProps()} />
|
||||
{!props.isValidating && props.items.length == 0 ? (
|
||||
<EmptyLibrary
|
||||
onAddLinkClicked={() => {
|
||||
props.setShowAddLinkModal(true)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LibraryItems
|
||||
items={props.items}
|
||||
layout={layout}
|
||||
viewer={viewerData?.me}
|
||||
gridContainerRef={props.gridContainerRef}
|
||||
setShowEditTitleModal={props.setShowEditTitleModal}
|
||||
setLinkToEdit={props.setLinkToEdit}
|
||||
setShowUnsubscribeConfirmation={
|
||||
setShowUnsubscribeConfirmation
|
||||
}
|
||||
setLinkToRemove={props.setLinkToRemove}
|
||||
setLinkToUnsubscribe={props.setLinkToUnsubscribe}
|
||||
setShowRemoveLinkConfirmation={
|
||||
setShowRemoveLinkConfirmation
|
||||
}
|
||||
actionHandler={props.actionHandler}
|
||||
/>
|
||||
)}
|
||||
<HStack
|
||||
distribution="center"
|
||||
css={{ width: '100%', mt: '$2', mb: '$4' }}
|
||||
>
|
||||
{props.hasMore ? (
|
||||
<Button
|
||||
style="ctaGray"
|
||||
{({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
|
||||
<div
|
||||
{...getRootProps({ className: 'dropzone' })}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
{inDragOperation && uploadingFiles.length < 1 && (
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<Box
|
||||
css={{
|
||||
cursor: props.isValidating ? 'not-allowed' : 'pointer',
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
}}
|
||||
onClick={props.loadMore}
|
||||
disabled={props.isValidating}
|
||||
>
|
||||
{props.isValidating ? 'Loading' : 'Load More'}
|
||||
</Button>
|
||||
) : (
|
||||
<StyledText style="caption"></StyledText>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</VStack>
|
||||
{props.showAddLinkModal && (
|
||||
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
|
||||
)}
|
||||
{props.showEditTitleModal && (
|
||||
<EditLibraryItemModal
|
||||
updateItem={(item: LibraryItem) =>
|
||||
props.actionHandler('update-item', item)
|
||||
}
|
||||
onOpenChange={() => props.setShowEditTitleModal(false)}
|
||||
item={props.linkToEdit as LibraryItem}
|
||||
/>
|
||||
)}
|
||||
{showRemoveLinkConfirmation && (
|
||||
<ConfirmationModal
|
||||
richMessage={
|
||||
<VStack alignment="center" distribution="center">
|
||||
<StyledText style="modalTitle" css={{ margin: '0px 8px' }}>
|
||||
Are you sure you want to delete this item? All associated
|
||||
notes and highlights will be deleted.
|
||||
</StyledText>
|
||||
{props.linkToRemove?.node && viewerData?.me && (
|
||||
<Box
|
||||
Drop PDF document to to upload and add to your library
|
||||
</Box>
|
||||
</DragnDropStyle>
|
||||
</DragnDropContainer>
|
||||
)}
|
||||
{uploadingFiles.length > 0 && (
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<Box
|
||||
css={{
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
width: '80%',
|
||||
}}
|
||||
>
|
||||
<Progress.Root
|
||||
className="ProgressRoot"
|
||||
value={uploadProgress}
|
||||
>
|
||||
<Progress.Indicator
|
||||
className="ProgressIndicator"
|
||||
style={{
|
||||
transform: `translateX(-${100 - uploadProgress}%)`,
|
||||
}}
|
||||
/>
|
||||
</Progress.Root>
|
||||
<StyledText
|
||||
style="boldText"
|
||||
css={{
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
Uploading file
|
||||
</StyledText>
|
||||
</Box>
|
||||
</DragnDropStyle>
|
||||
</DragnDropContainer>
|
||||
)}
|
||||
<input {...getInputProps()} />
|
||||
{!props.isValidating && props.items.length == 0 ? (
|
||||
<EmptyLibrary
|
||||
onAddLinkClicked={() => {
|
||||
props.setShowAddLinkModal(true)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LibraryItems
|
||||
items={props.items}
|
||||
layout={props.layout}
|
||||
viewer={props.viewer}
|
||||
gridContainerRef={props.gridContainerRef}
|
||||
setShowEditTitleModal={props.setShowEditTitleModal}
|
||||
setLinkToEdit={props.setLinkToEdit}
|
||||
setShowUnsubscribeConfirmation={
|
||||
setShowUnsubscribeConfirmation
|
||||
}
|
||||
setLinkToRemove={props.setLinkToRemove}
|
||||
setLinkToUnsubscribe={props.setLinkToUnsubscribe}
|
||||
setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation}
|
||||
actionHandler={props.actionHandler}
|
||||
/>
|
||||
)}
|
||||
<HStack
|
||||
distribution="center"
|
||||
css={{ width: '100%', mt: '$2', mb: '$4' }}
|
||||
>
|
||||
{props.hasMore ? (
|
||||
<Button
|
||||
style="ctaGray"
|
||||
css={{
|
||||
transform: 'scale(0.6)',
|
||||
opacity: 0.8,
|
||||
pointerEvents: 'none',
|
||||
filter: 'grayscale(1)',
|
||||
cursor: props.isValidating ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
onClick={props.loadMore}
|
||||
disabled={props.isValidating}
|
||||
>
|
||||
<LinkedItemCard
|
||||
item={props.linkToRemove?.node}
|
||||
viewer={viewerData.me}
|
||||
layout="GRID_LAYOUT"
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handleAction={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
{props.isValidating ? 'Loading' : 'Load More'}
|
||||
</Button>
|
||||
) : (
|
||||
<StyledText style="caption"></StyledText>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</VStack>
|
||||
|
||||
{props.showAddLinkModal && (
|
||||
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
|
||||
)}
|
||||
{props.showEditTitleModal && (
|
||||
<EditLibraryItemModal
|
||||
updateItem={(item: LibraryItem) =>
|
||||
props.actionHandler('update-item', item)
|
||||
}
|
||||
onOpenChange={() => props.setShowEditTitleModal(false)}
|
||||
item={props.linkToEdit as LibraryItem}
|
||||
/>
|
||||
)}
|
||||
{showRemoveLinkConfirmation && (
|
||||
<ConfirmationModal
|
||||
richMessage={
|
||||
<VStack alignment="center" distribution="center">
|
||||
<StyledText style="modalTitle" css={{ margin: '0px 8px' }}>
|
||||
Are you sure you want to delete this item? All associated notes
|
||||
and highlights will be deleted.
|
||||
</StyledText>
|
||||
{props.linkToRemove?.node && props.viewer && (
|
||||
<Box
|
||||
css={{
|
||||
transform: 'scale(0.6)',
|
||||
opacity: 0.8,
|
||||
pointerEvents: 'none',
|
||||
filter: 'grayscale(1)',
|
||||
}}
|
||||
>
|
||||
<LinkedItemCard
|
||||
item={props.linkToRemove?.node}
|
||||
viewer={props.viewer}
|
||||
layout="GRID_LAYOUT"
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handleAction={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
}
|
||||
onAccept={removeItem}
|
||||
acceptButtonLabel="Delete Item"
|
||||
onOpenChange={() => setShowRemoveLinkConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{showUnsubscribeConfirmation && (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure you want to unsubscribe?'}
|
||||
onAccept={unsubscribe}
|
||||
onOpenChange={() => setShowUnsubscribeConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{props.labelsTarget?.node.id && (
|
||||
<SetLabelsModal
|
||||
provider={props.labelsTarget.node}
|
||||
onLabelsUpdated={(labels: Label[]) => {
|
||||
if (props.labelsTarget) {
|
||||
props.labelsTarget.node.labels = labels
|
||||
updateState({})
|
||||
}
|
||||
onAccept={removeItem}
|
||||
acceptButtonLabel="Delete Item"
|
||||
onOpenChange={() => setShowRemoveLinkConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{showUnsubscribeConfirmation && (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure you want to unsubscribe?'}
|
||||
onAccept={unsubscribe}
|
||||
onOpenChange={() => setShowUnsubscribeConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{props.labelsTarget?.node.id && (
|
||||
<SetLabelsModal
|
||||
provider={props.labelsTarget.node}
|
||||
onLabelsUpdated={(labels: Label[]) => {
|
||||
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)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
}}
|
||||
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)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
14
yarn.lock
14
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue