Add suggestion boxes for the major pages
This commit is contained in:
116
packages/web/components/elements/SuggestionBox.tsx
Normal file
116
packages/web/components/elements/SuggestionBox.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import Link from 'next/link'
|
||||
import { HStack, SpanBox, VStack } from './LayoutPrimitives'
|
||||
import { ArrowRightIcon } from './icons/ArrowRightIcon'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { ReactNode } from 'react'
|
||||
import { Button } from './Button'
|
||||
import { CloseIcon } from './images/CloseIcon'
|
||||
|
||||
type SuggestionBoxProps = {
|
||||
helpMessage: string
|
||||
helpCTAText: string | undefined
|
||||
helpTarget: string | undefined
|
||||
|
||||
size?: 'large' | 'small'
|
||||
background?: string
|
||||
|
||||
dismissible?: boolean
|
||||
onDismiss?: () => void
|
||||
}
|
||||
|
||||
type InternalOrExternalLinkProps = {
|
||||
link: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const InternalOrExternalLink = (props: InternalOrExternalLinkProps) => {
|
||||
const isExternal = props.link.startsWith('https')
|
||||
|
||||
return (
|
||||
<SpanBox
|
||||
css={{
|
||||
cursor: 'pointer',
|
||||
a: {
|
||||
color: '$omnivoreCtaYellow',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!isExternal ? (
|
||||
<Link href={props.link}>{props.children}</Link>
|
||||
) : (
|
||||
<a href={props.link} target="_blank" rel="noreferrer">
|
||||
{props.children}
|
||||
</a>
|
||||
)}
|
||||
</SpanBox>
|
||||
)
|
||||
}
|
||||
|
||||
export const SuggestionBox = (props: SuggestionBoxProps) => {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: props.size == 'large' ? 'column' : 'row',
|
||||
width: 'fit-content',
|
||||
borderRadius: '5px',
|
||||
background: props.background ?? '$thBackground3',
|
||||
fontSize: '15px',
|
||||
fontFamily: '$inter',
|
||||
fontWeight: '500',
|
||||
color: '$thTextContrast',
|
||||
py: '10px',
|
||||
px: '15px',
|
||||
justifyContent: 'flex-start',
|
||||
'@smDown': {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VStack>
|
||||
{props.dismissible && (
|
||||
<SpanBox
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
fontSize: '10',
|
||||
fontWeight: '600',
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</SpanBox>
|
||||
)}
|
||||
{props.helpMessage}
|
||||
{props.helpTarget && (
|
||||
<InternalOrExternalLink link={props.helpTarget}>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '$omnivoreCtaYellow',
|
||||
gap: '2px',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<>{props.helpCTAText}</>
|
||||
<ArrowRightIcon
|
||||
size={25}
|
||||
color={theme.colors.omnivoreCtaYellow.toString()}
|
||||
/>
|
||||
</SpanBox>
|
||||
</InternalOrExternalLink>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
@ -49,7 +49,6 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
|
||||
<Box
|
||||
css={{
|
||||
height: HEADER_HEIGHT,
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
></Box>
|
||||
{props.children}
|
||||
|
||||
@ -9,6 +9,7 @@ import { searchQuery } from '../../../lib/networking/queries/search'
|
||||
import { LIBRARY_LEFT_MENU_WIDTH } from './LibraryFilterMenu'
|
||||
import { LayoutType } from './HomeFeedContainer'
|
||||
import { ArrowRightIcon } from '../../elements/icons/ArrowRightIcon'
|
||||
import { SuggestionBox } from '../../elements/SuggestionBox'
|
||||
|
||||
type EmptyLibraryProps = {
|
||||
searchTerm: string | undefined
|
||||
@ -17,7 +18,13 @@ type EmptyLibraryProps = {
|
||||
layoutType: LayoutType
|
||||
}
|
||||
|
||||
type MessageType = 'feed' | 'newsletter' | 'library'
|
||||
type MessageType =
|
||||
| 'inbox'
|
||||
| 'files'
|
||||
| 'archive'
|
||||
| 'feed'
|
||||
| 'newsletter'
|
||||
| 'library'
|
||||
|
||||
type HelpMessageProps = {
|
||||
type: MessageType
|
||||
@ -83,6 +90,12 @@ const HelpMessage = (props: HelpMessageProps) => {
|
||||
export const ErrorBox = (props: HelpMessageProps) => {
|
||||
const errorTitle = useMemo(() => {
|
||||
switch (props.type) {
|
||||
case 'inbox':
|
||||
return 'Your inbox is empty.'
|
||||
case 'archive':
|
||||
return 'You do not have any archived items.'
|
||||
case 'files':
|
||||
return 'No files found.'
|
||||
case 'feed':
|
||||
return 'You do not have any feed items matching this query.'
|
||||
case 'newsletter':
|
||||
@ -116,70 +129,70 @@ export const ErrorBox = (props: HelpMessageProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const SuggestionBox = (props: HelpMessageProps) => {
|
||||
export const Suggestion = (props: HelpMessageProps) => {
|
||||
const helpMessage = useMemo(() => {
|
||||
switch (props.type) {
|
||||
case 'feed':
|
||||
return 'Want to add an RSS or Atom Subscription?'
|
||||
return ['Want to add an RSS or Atom Subscription?', 'Click Here']
|
||||
case 'archive':
|
||||
return [
|
||||
'When you are done reading something archive it and it will be saved in Omnivore forever.',
|
||||
'Read the Docs',
|
||||
]
|
||||
case 'files':
|
||||
return [
|
||||
'Drag PDFs into the library to add them to your Omnivore account.',
|
||||
undefined,
|
||||
]
|
||||
case 'newsletter':
|
||||
return 'Create an Omnivore email address and subscribe to newsletters.'
|
||||
return [
|
||||
'Create an Omnivore email address and subscribe to newsletters.',
|
||||
'Click Here',
|
||||
]
|
||||
}
|
||||
return "Add a link or read more about Omnivore's Advanced Search."
|
||||
return [
|
||||
"Add a link or read more about Omnivore's Advanced Search.",
|
||||
'Read the Docs',
|
||||
]
|
||||
}, [props.type])
|
||||
|
||||
const helpTarget = useMemo(() => {
|
||||
switch (props.type) {
|
||||
case 'feed':
|
||||
return '/settings/feeds'
|
||||
case 'archive':
|
||||
case 'files':
|
||||
return undefined
|
||||
case 'archive':
|
||||
case 'inbox':
|
||||
return 'https://docs.omnivore.app/using/saving'
|
||||
case 'newsletter':
|
||||
return '/settings/emails'
|
||||
}
|
||||
return 'https://docs.omnivore.app/'
|
||||
}, [props.type])
|
||||
|
||||
const helpTargetWindow = useMemo(() => {
|
||||
switch (props.type) {
|
||||
case 'archive':
|
||||
case 'inbox':
|
||||
return '_blank'
|
||||
}
|
||||
return undefined
|
||||
}, [props.type])
|
||||
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
width: 'fit-content',
|
||||
borderRadius: '5px',
|
||||
background: '$thBackground3',
|
||||
fontSize: '15px',
|
||||
fontFamily: '$inter',
|
||||
fontWeight: '500',
|
||||
color: '$thTextContrast',
|
||||
padding: '10px',
|
||||
justifyContent: 'flex-start',
|
||||
'@smDown': {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{helpMessage}
|
||||
<SpanBox css={{ cursor: 'pointer' }}>
|
||||
<Link href={helpTarget} passHref>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '$omnivoreCtaYellow',
|
||||
gap: '2px',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<>Click Here</>
|
||||
<ArrowRightIcon
|
||||
size={25}
|
||||
color={theme.colors.omnivoreCtaYellow.toString()}
|
||||
/>
|
||||
</SpanBox>
|
||||
</Link>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
<>
|
||||
{helpMessage[0] ? (
|
||||
<SuggestionBox
|
||||
helpMessage={helpMessage[0]}
|
||||
helpCTAText={helpMessage[1]}
|
||||
helpTarget={helpTarget}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -187,6 +200,12 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => {
|
||||
const type = useMemo<MessageType>(() => {
|
||||
if (props.searchTerm) {
|
||||
switch (props.searchTerm) {
|
||||
case 'in:archive':
|
||||
return 'archive'
|
||||
case 'in:inbox':
|
||||
return 'inbox'
|
||||
case 'type:file':
|
||||
return 'files'
|
||||
case 'label:RSS':
|
||||
return 'feed'
|
||||
case 'label:Newsletter':
|
||||
@ -205,9 +224,7 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => {
|
||||
pl: '0px',
|
||||
|
||||
width: '100%',
|
||||
'@media (max-width: 1300px)': {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
flexDirection: 'column',
|
||||
|
||||
'@media (max-width: 768px)': {
|
||||
p: '15px',
|
||||
@ -232,7 +249,7 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => {
|
||||
}}
|
||||
>
|
||||
<ErrorBox type={type} />
|
||||
<SuggestionBox type={type} />
|
||||
<Suggestion type={type} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { SettingsLayout } from '../SettingsLayout'
|
||||
import { SuggestionBox } from '../../elements/SuggestionBox'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
|
||||
type SettingsTableProps = {
|
||||
pageId: string
|
||||
@ -17,6 +19,8 @@ type SettingsTableProps = {
|
||||
createTitle?: string
|
||||
createAction?: () => void
|
||||
|
||||
suggestionInfo: SuggestionInfo
|
||||
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
@ -52,6 +56,12 @@ type MoreOptionsProps = {
|
||||
onEdit?: () => void
|
||||
}
|
||||
|
||||
type SuggestionInfo = {
|
||||
text: string
|
||||
docs: string
|
||||
key: string
|
||||
}
|
||||
|
||||
const MoreOptions = (props: MoreOptionsProps) => (
|
||||
<Dropdown
|
||||
align={'end'}
|
||||
@ -273,6 +283,11 @@ const CreateButton = (props: CreateButtonProps): JSX.Element => {
|
||||
}
|
||||
|
||||
export const SettingsTable = (props: SettingsTableProps): JSX.Element => {
|
||||
const [showSuggestion, setShowSuggestion] = usePersistedState<boolean>({
|
||||
key: props.suggestionInfo.key,
|
||||
initialValue: !!props.suggestionInfo,
|
||||
})
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Toaster
|
||||
@ -299,6 +314,21 @@ export const SettingsTable = (props: SettingsTableProps): JSX.Element => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.suggestionInfo && showSuggestion && (
|
||||
<Box css={{ my: '40px', width: '100%' }}>
|
||||
<SuggestionBox
|
||||
helpMessage={props.suggestionInfo.text}
|
||||
helpCTAText={'Read the Docs'}
|
||||
helpTarget={props.suggestionInfo.docs}
|
||||
size={'large'}
|
||||
background="$thBackground5"
|
||||
dismissible={true}
|
||||
onDismiss={() => {
|
||||
setShowSuggestion(false)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
|
||||
@ -121,6 +121,11 @@ export default function Api(): JSX.Element {
|
||||
setExpiresAt(neverExpiresDate)
|
||||
setAddModalOpen(true)
|
||||
}}
|
||||
suggestionInfo={{
|
||||
text: 'Create API keys to connect Omnivore to other apps such as Logseq and Obsidian or to query the API. Check out the integrations page for more info on connecting to Omnivore via the API.',
|
||||
docs: 'https://docs.omnivore.app/integrations/api.html',
|
||||
key: '--settings-apikeys-show-help',
|
||||
}}
|
||||
>
|
||||
{sortedApiKeys.length > 0 ? (
|
||||
sortedApiKeys.map((apiKey, i) => {
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
SettingsTableRow,
|
||||
} from '../../../components/templates/settings/SettingsTable'
|
||||
import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
|
||||
import { SuggestionBox } from '../../../components/elements/SuggestionBox'
|
||||
|
||||
enum TextType {
|
||||
EmailAddress,
|
||||
@ -121,6 +122,11 @@ export default function EmailsPage(): JSX.Element {
|
||||
headerTitle="Address"
|
||||
createTitle="Create a new email address"
|
||||
createAction={createEmail}
|
||||
suggestionInfo={{
|
||||
text: 'Create an Omnivore email address and use it to subscribe to newsletters. The newsletters will be automatically added to your library.',
|
||||
docs: 'https://docs.omnivore.app/using/inbox.html',
|
||||
key: '--settings-emails-show-help',
|
||||
}}
|
||||
>
|
||||
{sortedEmailAddresses.length > 0 ? (
|
||||
sortedEmailAddresses.map((email, i) => {
|
||||
|
||||
@ -168,9 +168,14 @@ export default function RecentEmails(): JSX.Element {
|
||||
|
||||
return (
|
||||
<SettingsTable
|
||||
pageId="api-keys"
|
||||
pageId="recent-emails"
|
||||
pageInfoLink="https://docs.omnivore.app/using/inbox.html"
|
||||
headerTitle="Recently Received Emails"
|
||||
suggestionInfo={{
|
||||
text: 'View all the original content of emails that have been recently received in your Omnivore inbox. If an email was not correctly classified as an article you can mark it as an article.',
|
||||
docs: 'https://docs.omnivore.app/using/inbox.html',
|
||||
key: '--settings-recent-emails-show-help',
|
||||
}}
|
||||
>
|
||||
{sortedRecentEmails.length > 0 ? (
|
||||
sortedRecentEmails.map((recentEmail: RecentEmail, i) => {
|
||||
|
||||
@ -96,6 +96,11 @@ export default function Rss(): JSX.Element {
|
||||
createAction={() => {
|
||||
router.push('/settings/feeds/add')
|
||||
}}
|
||||
suggestionInfo={{
|
||||
text: 'Add RSS and Atom feeds to your Omnivore account. When you add a new feed the last 24hrs of items, or at least one item will be added to your account.',
|
||||
docs: 'https://docs.omnivore.app/using/feeds.html',
|
||||
key: '--settings-feeds-show-help',
|
||||
}}
|
||||
>
|
||||
{subscriptions.length === 0 ? (
|
||||
<EmptySettingsRow text={isValidating ? '-' : 'No feeds subscribed'} />
|
||||
|
||||
@ -35,6 +35,8 @@ import {
|
||||
import { LabelChip } from '../../components/elements/LabelChip'
|
||||
import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
|
||||
import { InfoLink } from '../../components/elements/InfoLink'
|
||||
import { SuggestionBox } from '../../components/elements/SuggestionBox'
|
||||
import { usePersistedState } from '../../lib/hooks/usePersistedState'
|
||||
|
||||
const HeaderWrapper = styled(Box, {
|
||||
width: '100%',
|
||||
@ -153,6 +155,10 @@ export default function LabelsPage(): JSX.Element {
|
||||
const [confirmRemoveLabelId, setConfirmRemoveLabelId] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
const [showLabelPageHelp, setShowLabelPageHelp] = usePersistedState<boolean>({
|
||||
key: `--settings-labels-show-help`,
|
||||
initialValue: true,
|
||||
})
|
||||
const breakpoint = 768
|
||||
|
||||
applyStoredTheme(false)
|
||||
@ -270,6 +276,21 @@ export default function LabelsPage(): JSX.Element {
|
||||
onOpenChange={() => setConfirmRemoveLabelId(null)}
|
||||
/>
|
||||
) : null}
|
||||
{showLabelPageHelp && (
|
||||
<Box css={{ mt: '40px' }}>
|
||||
<SuggestionBox
|
||||
helpMessage="Use this page to view and edit all your labels. Labels can be attached to individual library items, or your highlights, and are used to keep your library organized."
|
||||
helpCTAText={'Read the Docs'}
|
||||
helpTarget="https://docs.omnivore.app/using/organizing.html"
|
||||
size={'large'}
|
||||
background="$thBackground5"
|
||||
dismissible={true}
|
||||
onDismiss={() => {
|
||||
setShowLabelPageHelp(false)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<HeaderWrapper>
|
||||
<Box
|
||||
style={{
|
||||
|
||||
@ -44,8 +44,13 @@ export default function SubscriptionsPage(): JSX.Element {
|
||||
return (
|
||||
<SettingsTable
|
||||
pageId="settings-subscriptions-tag"
|
||||
pageInfoLink="https://docs.omnivore.app/using/inbox.html"
|
||||
pageInfoLink="https://docs.omnivore.app/using/feeds.html"
|
||||
headerTitle="Subscriptions"
|
||||
suggestionInfo={{
|
||||
text: 'View and manage all your Feed and Newsletter subscriptions. You can add RSS or Atom subscriptions on the Feeds page and create emails to subscribe to newsletters',
|
||||
docs: 'https://docs.omnivore.app/using/inbox.html',
|
||||
key: '--settings-recent-subscriptions-show-help',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{sortedSubscriptions.length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user