Merge pull request #464 from omnivore-app/fix/empty-state

Display empty state when no library items are found
This commit is contained in:
Jackson Harper 2022-04-22 10:25:34 -07:00 committed by GitHub
commit 330f5c07c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 5536 additions and 300 deletions

View file

@ -0,0 +1,36 @@
// There aren't any discussions.
// You can open a new discussion to ask questions about this repository or get help.
import Link from 'next/link'
import { Book } from 'phosphor-react'
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { Button } from '../../elements/Button'
import { Box, VStack } from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes'
import { theme } from '../../tokens/stitches.config'
type EmptyLibraryProps = {
onAddLinkClicked: () => void
}
export function EmptyLibrary(props: EmptyLibraryProps): JSX.Element {
return (
<VStack alignment="center" distribution="center" css={{ color: '$grayTextContrast', textAlign: 'center', paddingTop: '88px' }}>
<Book size={44} color={theme.colors.grayTextContrast.toString()} />
<StyledText style="fixedHeadline" css={{ color: '$grayTextContrast' }}>
No results found.
</StyledText>
<StyledText style="footnote" css={{ color: '$grayTextContrast' }}>
You can add a link or read more about Omnivore&apos;s <Link href="/help/search">advanced search</Link>.
</StyledText>
<Button style="ctaDarkYellow" onClick={() => { props.onAddLinkClicked() }}>
Add Link
</Button>
</VStack>
)
}

View file

@ -38,6 +38,9 @@ import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { SetLabelsModal } from '../article/SetLabelsModal'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { isVipUser } from '../../../lib/featureFlag'
import { EmptyLibrary } from './EmptyLibrary'
import TopBarProgress from 'react-topbar-progress-indicator'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
@ -47,10 +50,10 @@ export type HomeFeedContainerProps = {
const SAVED_SEARCHES: Record<string,string> = {
'Inbox': '',
'All': 'in:all',
'Read Later': `in:inbox -label:Newsletter`,
'Highlighted': `in:inbox has:highlights`,
'Today': `in:inbox saved:${new Date(new Date().getTime() - (24 * 3600000)).toISOString().split('T')[0]}..*`,
'Newsletter': `in:inbox label:Newsletter`,
'Non-Newsletter': `in:inbox -label:Newsletter`,
'Newsletters': `in:inbox label:Newsletter`,
}
export function HomeFeedContainer(props: HomeFeedContainerProps): JSX.Element {
@ -532,6 +535,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
}}
>
<Toaster />
{(props.isValidating && props.items.length == 0 && <TopBarProgress />)}
<HStack alignment="center" distribution="start" css={{ width: '100%' }}>
<StyledText
style="subHeadline"
@ -596,7 +600,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
const searchQuery = SAVED_SEARCHES[key]
const style = searchQuery === props.searchTerm || (!props.searchTerm && !searchQuery) ? 'ctaDarkYellow' : 'ctaLightGray'
return (
<Button key={key} style={style} onClick={() => { props.applySearchQuery(searchQuery)}} css={{ p: '10px 12px', borderRadius: '6px', whiteSpace: 'nowrap' }}>
<Button key={key} style={style} onClick={() => { props.applySearchQuery(searchQuery)}} css={{ p: '10px 12px', height: '37.5px', borderRadius: '6px', whiteSpace: 'nowrap' }}>
{key}
</Button>
)
@ -604,95 +608,97 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
}
</Box>
)}
<Box
ref={props.gridContainerRef}
css={{
py: '$3',
display: 'grid',
width: '100%',
gridAutoRows: 'auto',
borderRadius: '8px',
gridGap: layout == 'LIST_LAYOUT' ? '0' : '$3',
marginTop: layout == 'LIST_LAYOUT' ? '21px' : '0',
marginBottom: '0px',
paddingTop: layout == 'LIST_LAYOUT' ? '0' : '21px',
paddingBottom: layout == 'LIST_LAYOUT' ? '0px' : '21px',
overflow: 'hidden',
'@smDown': {
border: 'unset',
width: layout == 'LIST_LAYOUT' ? '100vw' : undefined,
margin: layout == 'LIST_LAYOUT' ? '16px -16px' : undefined,
borderRadius: layout == 'LIST_LAYOUT' ? 0 : undefined,
},
'@md': {
gridTemplateColumns: layout == 'LIST_LAYOUT' ? 'none' : '1fr 1fr',
},
'@lg': {
gridTemplateColumns:
layout == 'LIST_LAYOUT' ? 'none' : 'repeat(3, 1fr)',
},
}}
>
{props.items.map((linkedItem) => (
<Box
className="linkedItemCard"
id={linkedItem.node.id}
tabIndex={0}
key={linkedItem.node.id}
css={{
width: '100%',
'&> div': {
bg: '$grayBg',
},
'&:focus': {
'> div': {
bg: '$grayBgActive',
{!props.isValidating && props.items.length == 0 ? <EmptyLibrary onAddLinkClicked={() => { props.setShowAddLinkModal(true) }} /> : (
<Box
ref={props.gridContainerRef}
css={{
py: '$3',
display: 'grid',
width: '100%',
gridAutoRows: 'auto',
borderRadius: '8px',
gridGap: layout == 'LIST_LAYOUT' ? '0' : '$3',
marginTop: layout == 'LIST_LAYOUT' ? '21px' : '0',
marginBottom: '0px',
paddingTop: layout == 'LIST_LAYOUT' ? '0' : '21px',
paddingBottom: layout == 'LIST_LAYOUT' ? '0px' : '21px',
overflow: 'hidden',
'@smDown': {
border: 'unset',
width: layout == 'LIST_LAYOUT' ? '100vw' : undefined,
margin: layout == 'LIST_LAYOUT' ? '16px -16px' : undefined,
borderRadius: layout == 'LIST_LAYOUT' ? 0 : undefined,
},
'@md': {
gridTemplateColumns: layout == 'LIST_LAYOUT' ? 'none' : '1fr 1fr',
},
'@lg': {
gridTemplateColumns:
layout == 'LIST_LAYOUT' ? 'none' : 'repeat(3, 1fr)',
},
}}
>
{props.items.map((linkedItem) => (
<Box
className="linkedItemCard"
id={linkedItem.node.id}
tabIndex={0}
key={linkedItem.node.id}
css={{
width: '100%',
'&> div': {
bg: '$grayBg',
},
},
'&:hover': {
'> div': {
bg: '$grayBgActive',
'&:focus': {
'> div': {
bg: '$grayBgActive',
},
},
},
}}
>
{viewerData?.me && (
<LinkedItemCard
layout={layout}
item={linkedItem.node}
viewer={viewerData.me}
handleAction={(action: LinkedItemCardAction) => {
if (action === 'delete') {
setShowRemoveLinkConfirmation(true)
setLinkToRemove(linkedItem)
} else {
props.actionHandler(action, linkedItem)
}
}}
/>
)}
</Box>
))}
</Box>
<HStack
distribution="center"
css={{ width: '100%', mt: '$2', mb: '$4' }}
>
{props.hasMore ? (
<Button
style="ctaGray"
css={{
cursor: props.isValidating ? 'not-allowed' : 'pointer',
}}
onClick={props.loadMore}
disabled={props.isValidating}
>
{props.isValidating ? 'Loading' : 'Load More'}
</Button>
) : (
<StyledText style="caption"></StyledText>
'&:hover': {
'> div': {
bg: '$grayBgActive',
},
},
}}
>
{viewerData?.me && (
<LinkedItemCard
layout={layout}
item={linkedItem.node}
viewer={viewerData.me}
handleAction={(action: LinkedItemCardAction) => {
if (action === 'delete') {
setShowRemoveLinkConfirmation(true)
setLinkToRemove(linkedItem)
} else {
props.actionHandler(action, linkedItem)
}
}}
/>
)}
</Box>
))}
</Box>
)}
</HStack>
<HStack
distribution="center"
css={{ width: '100%', mt: '$2', mb: '$4' }}
>
{props.hasMore ? (
<Button
style="ctaGray"
css={{
cursor: props.isValidating ? 'not-allowed' : 'pointer',
}}
onClick={props.loadMore}
disabled={props.isValidating}
>
{props.isValidating ? 'Loading' : 'Load More'}
</Button>
) : (
<StyledText style="caption"></StyledText>
)}
</HStack>
</VStack>
{props.showAddLinkModal && (
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />

View file

@ -42,6 +42,7 @@
"react-hot-toast": "^2.1.1",
"react-intl": "^5.20.12",
"react-loading-skeleton": "^3.0.2",
"react-topbar-progress-indicator": "^4.1.1",
"react-twitter-widgets": "^1.10.0",
"swr": "^1.0.1",
"uuid": "^8.3.2"

View file

@ -9,6 +9,16 @@ import { useEffect, useState } from 'react'
import { Analytics, AnalyticsBrowser } from '@segment/analytics-next'
import { segmentApiKey } from '../lib/appConfig'
import { TooltipProvider } from '../components/elements/Tooltip'
import TopBarProgress from 'react-topbar-progress-indicator'
TopBarProgress.config({
barColors: {
"0": '#FFD234',
"1.0": '#FFD234',
},
shadowBlur: 0,
barThickness: 2,
});
function OmnivoreApp({ Component, pageProps }: AppProps): JSX.Element {
const router = useRouter()

View file

@ -0,0 +1,140 @@
/* eslint-disable @next/next/no-img-element */
import { Box, HStack, SpanBox } from '../../components/elements/LayoutPrimitives'
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
import { Button } from '../../components/elements/Button'
import Link from 'next/link'
import { Copy, Plus } from 'phosphor-react'
import { theme } from '../../components/tokens/stitches.config'
export default function Search(): JSX.Element {
return (
<PrimaryLayout
pageMetaDataProps={{
title: 'Search',
path: '/help/search',
}}
pageTestId="help-search-tag"
>
<Box
css={{
m: '42px',
maxWidth: '640px',
color: '$grayText',
img: {
maxWidth: '85%',
},
'@smDown': {
m: '16px',
maxWidth: '85%',
alignSelf: 'center',
},
}}
>
<h1>Search</h1>
<hr />
<p>
Omnivore uses search to filter items in your library. You can use simple
keyword search to find items or our advanced search syntax.
</p>
<ul>
<li><a href="#text">Searching for text</a></li>
<li><a href="#label">Filtering by label</a></li>
<li><a href="#in">Filtering by archive status</a></li>
<li><a href="#is">Filtering by read state</a></li>
<li><a href="#type">Filtering by type</a></li>
<li><a href="#has">Finding highlights</a></li>
<li><a href="#dates">Filtering by save/publish dates</a></li>
<li><a href="#sort">Sorting</a></li>
</ul>
<h2 id="text">Searching for text</h2>
<p>
Omnivore will perform full text search across library item&apos;s content, title, description,
and site by default. You can search for specific terms by quoting your terms. By default
all results that match your search will be returned in the order they were saved. To change
your search to relevance use the <code>sort:score</code> parameter.
</p>
<h2 id="label">Filtering by label</h2>
<p>
You can filter your search based on labels using AND and OR clauses.
You can also negate a label search to find pages that do not have a certain label.
</p>
<p>Some examples:</p>
<ul>
<li>label:Newsletter finds all pages that have the label Newsletter</li>
<li>label:Cooking,Fitness finds all your pages with either the Cooking or Fitness labels</li>
<li>label:Newsletter label:Surfing finds all pages with both the Newsletter and Surfing labels</li>
<li>label:Coding -label:News finds all pages with the Coding label that do not have the News label</li>
</ul>
<h2 id="in">Filtering by archive status</h2>
<p>
The <code>in:</code> filter is used to filter search by archive status.
The options are:
</p>
<ul>
<li><code>in:inbox</code> (the default): show unarchived items</li>
<li><code>in:archive</code>: show archived items</li>
<li><code>in:all</code>: Show all items regardless of archive state</li>
</ul>
<h2 id="is">Filtering by read state</h2>
<p>
The <code>is:</code> filter is used to filter search by read state. Note
that in Omnivore &apos;read&apos; means fully read, not just opened.
</p>
<p>The <code>is:</code> filter options are: </p>
<ul>
<li><code>is:read</code>: Show only items that are fully read</li>
<li><code>is:unread</code> (the default): show unread items</li>
</ul>
<h2 id="type">Filtering by type</h2>
<p>
The <code>type:</code> filter is used to filter search by type.
</p>
<ul>
<li><code>type:article</code>: Show only articles</li>
<li><code>type:file</code> or <code>type:pdf</code>: Show only PDFs</li>
<li><code>type:highlights</code>: Show your highlights</li>
</ul>
<h2 id="has">Finding highlights</h2>
<p>
You can find your highlights by using the <code>type:highlights</code> filter
or find saved items with highlights using the <code>has:highlights</code> filter.
</p>
<h2 id="dates">Filtering by save/publish dates</h2>
<p>
You can filter your searches based on the time they were saved or published using the
<code>saved:</code> and <code>published:</code> filters. These filters take two dates
to create a date range. The <code>*</code> wildcard will accept any date.
</p>
<p>For Example:</p>
<ul>
<li><code>saved:2022-04-21..*</code> All items saved since 2022-04-21</li>
<li><code>published:2020-01-01..2022-02-02</code> All items published between 2020-01-01 and 2022-02-02</li>
<li><code>published:*..2020-01-01</code> All items published before 2020-01-01</li>
</ul>
<h2 id="sort">Sorting</h2>
<p>
By default all search results in Omnivore are sorted by saved date. This puts the
most recently saved items at the top of your library. You can use sort options
to change the library order:
</p>
<ul>
<li><code>sort:saved</code>: Sort by saved date</li>
<li><code>sort:updated</code>: Sort by time the item was updated, for example having a label or highlight added.</li>
<li><code>sort:score</code>: Sort by query term relevance.</li>
</ul>
</Box>
<Box css={{ height: '120px' }} />
</PrimaryLayout>
)
}

View file

@ -0,0 +1,21 @@
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { EmptyLibrary } from '../components/templates/homeFeed/EmptyLibrary'
export default {
title: 'Components/EmptyLibraryStory',
component: EmptyLibrary,
argTypes: {
position: {
description: 'The empty library component',
control: { type: 'select' },
},
},
} as ComponentMeta<typeof EmptyLibrary>
export const EmptyLibraryStory: ComponentStory<typeof EmptyLibrary> = (args: any) => {
return (
<EmptyLibrary onAddLinkClicked={() => {
console.log('onAddLinkClicked')
}} />
)
}

5442
yarn.lock

File diff suppressed because it is too large Load diff