mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
add basic highlights web UI
This commit is contained in:
parent
28255e073f
commit
470413b704
4 changed files with 186 additions and 12 deletions
|
|
@ -290,6 +290,11 @@ export const searchHighlights = async (
|
|||
const queryBuilder = tx
|
||||
.getRepository(Highlight)
|
||||
.createQueryBuilder('highlight')
|
||||
.innerJoin(
|
||||
'highlight.libraryItem',
|
||||
'libraryItem',
|
||||
'highlight.libraryItemId = libraryItem.id AND libraryItem.deletedAt IS NULL'
|
||||
)
|
||||
.andWhere('highlight.userId = :userId', { userId })
|
||||
.orderBy('highlight.updatedAt', 'DESC')
|
||||
.take(limit)
|
||||
|
|
|
|||
|
|
@ -210,10 +210,10 @@ const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => {
|
|||
filterTerm="in:library use:folders"
|
||||
icon={<LibraryIcon color={theme.colors.ctaBlue.toString()} />}
|
||||
/>
|
||||
<NavButton
|
||||
<NavRedirectButton
|
||||
{...props}
|
||||
text="Highlights"
|
||||
filterTerm="in:all has:highlights mode:highlights"
|
||||
redirectLocation={'/highlights'}
|
||||
icon={<HighlightsIcon color={theme.colors.highlight.toString()} />}
|
||||
/>
|
||||
<NavRedirectButton
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@ import { gqlFetcher } from '../networkHelpers'
|
|||
import { PageInfo } from './useGetLibraryItemsQuery'
|
||||
|
||||
interface HighlightsResponse {
|
||||
highlightsData?: Array<HighlightsData>
|
||||
highlightsError?: unknown
|
||||
data?: Array<HighlightsData>
|
||||
error?: unknown
|
||||
isLoading: boolean
|
||||
isValidating: boolean
|
||||
error: boolean
|
||||
size: number
|
||||
setSize: (
|
||||
size: number | ((_size: number) => number)
|
||||
|
|
@ -19,7 +18,6 @@ interface HighlightsResponse {
|
|||
|
||||
interface HighlightsVariables {
|
||||
first?: number
|
||||
after?: string
|
||||
query?: string
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +44,11 @@ export const useGetHighlights = (
|
|||
edges {
|
||||
node {
|
||||
...HighlightFields
|
||||
libraryItem {
|
||||
id
|
||||
title
|
||||
author
|
||||
}
|
||||
}
|
||||
cursor
|
||||
}
|
||||
|
|
@ -67,12 +70,13 @@ export const useGetHighlights = (
|
|||
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||
if (previousPageData && !previousPageData.highlights.edges) return null
|
||||
|
||||
if (pageIndex === 0) return `${query}_${variables.first}_${variables.query}`
|
||||
if (pageIndex === 0) return '0'
|
||||
|
||||
return `${query}_${previousPageData.highlights.pageInfo.endCursor}_${variables.first}_${variables.query}`
|
||||
return previousPageData.highlights.pageInfo.endCursor
|
||||
}
|
||||
|
||||
const fetcher = async () => gqlFetcher(query, variables, true)
|
||||
const fetcher = async (cursor: string | null) =>
|
||||
gqlFetcher(query, { ...variables, after: cursor }, true)
|
||||
|
||||
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
|
|
@ -99,10 +103,9 @@ export const useGetHighlights = (
|
|||
|
||||
return {
|
||||
isValidating,
|
||||
highlightsData: responsePages || undefined,
|
||||
highlightsError: responseError,
|
||||
data: responsePages || undefined,
|
||||
error: responseError,
|
||||
isLoading: !error && !data,
|
||||
error: !!error,
|
||||
size,
|
||||
setSize,
|
||||
mutate,
|
||||
|
|
|
|||
166
packages/web/pages/highlights.tsx
Normal file
166
packages/web/pages/highlights.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { LabelChip } from '../components/elements/LabelChip'
|
||||
import { Box, HStack, VStack } from '../components/elements/LayoutPrimitives'
|
||||
import { EmptyHighlights } from '../components/templates/homeFeed/EmptyHighlights'
|
||||
import { LibraryFilterMenu } from '../components/templates/navMenu/LibraryMenu'
|
||||
import { useApplyLocalTheme } from '../lib/hooks/useApplyLocalTheme'
|
||||
import { useFetchMore } from '../lib/hooks/useFetchMoreScroll'
|
||||
import { Highlight } from '../lib/networking/fragments/highlightFragment'
|
||||
import { useGetHighlights } from '../lib/networking/queries/useGetHighlights'
|
||||
import { highlightColor } from '../lib/themeUpdater'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export default function HighlightsPage(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false)
|
||||
const [_, setShowAddLinkModal] = useState(false)
|
||||
|
||||
useApplyLocalTheme()
|
||||
|
||||
const { isLoading, isValidating, mutate, setSize, size, data, error } =
|
||||
useGetHighlights({
|
||||
first: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const hasMore = useMemo(() => {
|
||||
if (!data) {
|
||||
return false
|
||||
}
|
||||
return data[data.length - 1].highlights.pageInfo.hasNextPage
|
||||
}, [data])
|
||||
|
||||
const handleFetchMore = useCallback(() => {
|
||||
if (isLoading || !hasMore) {
|
||||
return
|
||||
}
|
||||
setSize(size + 1)
|
||||
}, [isLoading, hasMore, setSize, size])
|
||||
|
||||
useFetchMore(handleFetchMore)
|
||||
|
||||
const highlights = useMemo(() => {
|
||||
if (!data) {
|
||||
return []
|
||||
}
|
||||
return data.flatMap((res) => res.highlights.edges.map((edge) => edge.node))
|
||||
}, [data])
|
||||
|
||||
if (!highlights.length) {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
height: `100vh`,
|
||||
}}
|
||||
>
|
||||
<EmptyHighlights />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack>
|
||||
<LibraryFilterMenu
|
||||
setShowAddLinkModal={setShowAddLinkModal}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
searchTerm={undefined}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
router?.push(`/home?q=${searchQuery}`)
|
||||
}}
|
||||
/>
|
||||
<VStack
|
||||
css={{
|
||||
maxWidth: '70%',
|
||||
padding: '20px',
|
||||
margin: '30px 50px 0 0',
|
||||
}}
|
||||
>
|
||||
{highlights.map((highlight) => {
|
||||
return <HighlightCard key={highlight.id} highlight={highlight} />
|
||||
})}
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
interface HighlightCardProps {
|
||||
highlight: Highlight
|
||||
}
|
||||
|
||||
function HighlightCard(props: HighlightCardProps): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
fontFamily: '$inter',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
bg: '$thBackground2',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
width: '30px',
|
||||
height: '5px',
|
||||
backgroundColor: highlightColor(props.highlight.color),
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
{props.highlight.quote && (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{props.highlight.quote}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
{props.highlight.labels && (
|
||||
<HStack
|
||||
css={{
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{props.highlight.labels.map((label) => {
|
||||
return (
|
||||
<LabelChip
|
||||
key={label.id}
|
||||
color={label.color}
|
||||
text={label.name}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</HStack>
|
||||
)}
|
||||
<Box
|
||||
css={{
|
||||
color: '$thText',
|
||||
fontSize: '12px',
|
||||
lineHeight: '20px',
|
||||
fontWeight: 300,
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
{props.highlight.libraryItem?.title}
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
color: '$grayText',
|
||||
fontSize: '12px',
|
||||
lineHeight: '20px',
|
||||
fontWeight: 300,
|
||||
}}
|
||||
>
|
||||
{props.highlight.libraryItem?.author}
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue