mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #3540 from omnivore-app/feat/web-new-left-menu
WIP on left menu improvements
This commit is contained in:
commit
96e3fdb8c3
29 changed files with 2053 additions and 167 deletions
|
|
@ -1,20 +1,13 @@
|
|||
import { SpanBox, HStack } from './LayoutPrimitives'
|
||||
import { Circle, X } from 'phosphor-react'
|
||||
import { isDarkTheme } from '../../lib/themeUpdater'
|
||||
|
||||
type LabelChipProps = {
|
||||
text: string
|
||||
color: string // expected to be a RGB hex color string
|
||||
isSelected?: boolean
|
||||
useAppAppearance?: boolean
|
||||
}
|
||||
|
||||
export function LabelChip(props: LabelChipProps): JSX.Element {
|
||||
const isDark = isDarkTheme()
|
||||
|
||||
const selectedBorder = isDark ? '#FFEA9F' : 'black'
|
||||
const unSelectedBorder = isDark ? '#2A2A2A' : '#F5F5F5'
|
||||
|
||||
return (
|
||||
<SpanBox
|
||||
css={{
|
||||
|
|
@ -30,9 +23,11 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
|
|||
borderRadius: '5px',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
color: isDark ? '#EBEBEB' : '#2A2A2A',
|
||||
borderColor: props.isSelected ? selectedBorder : unSelectedBorder,
|
||||
backgroundColor: isDark ? '#2A2A2A' : '#F5F5F5',
|
||||
color: '$thLabelChipForeground',
|
||||
borderColor: props.isSelected
|
||||
? '$thLabelChipSelectedBorder'
|
||||
: '$thLabelChipUnselectedBorder',
|
||||
backgroundColor: '$thLabelChipBackground',
|
||||
}}
|
||||
>
|
||||
<HStack alignment="center" css={{ gap: '5px' }}>
|
||||
|
|
|
|||
35
packages/web/components/elements/OutlinedLabelChip.tsx
Normal file
35
packages/web/components/elements/OutlinedLabelChip.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { SpanBox, HStack } from './LayoutPrimitives'
|
||||
import { Circle, X } from 'phosphor-react'
|
||||
|
||||
type LabelChipProps = {
|
||||
text: string
|
||||
color: string // expected to be a RGB hex color string
|
||||
isSelected?: boolean
|
||||
}
|
||||
|
||||
export function OutlinedLabelChip(props: LabelChipProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
gap: '10px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
fontFamily: '$inter',
|
||||
padding: '2.5px 12px',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer',
|
||||
backgroundClip: 'padding-box',
|
||||
borderRadius: '5px',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: '$thLabelOutlineChipBorder',
|
||||
color: '$thLabelChipForeground',
|
||||
}}
|
||||
>
|
||||
<Circle size={14} color={props.color} weight="fill" />
|
||||
<SpanBox css={{ pr: '2px' }}>{props.text}</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -37,6 +37,23 @@ const textVariants = {
|
|||
my: '6px',
|
||||
color: '$grayText',
|
||||
},
|
||||
settingsSection: {
|
||||
fontWeight: '600',
|
||||
fontSize: '17px',
|
||||
fontFamily: '$inter',
|
||||
color: '$grayText',
|
||||
m: '0px',
|
||||
my: '15px',
|
||||
marginBlockStart: '0px',
|
||||
marginBlockEnd: '0px',
|
||||
},
|
||||
settingsItem: {
|
||||
fontSize: '13px',
|
||||
fontFamily: '$display',
|
||||
color: '$grayText',
|
||||
marginBlockStart: '0px',
|
||||
marginBlockEnd: '0px',
|
||||
},
|
||||
userNote: {
|
||||
fontSize: '16px',
|
||||
paddingTop: '0px',
|
||||
|
|
|
|||
73
packages/web/components/elements/icons/DragIcon.tsx
Normal file
73
packages/web/components/elements/icons/DragIcon.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* eslint-disable functional/no-class */
|
||||
/* eslint-disable functional/no-this-expression */
|
||||
import { IconProps } from './IconProps'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export class DragIcon extends React.Component<IconProps> {
|
||||
render() {
|
||||
const size = (this.props.size || 26).toString()
|
||||
const color = (this.props.color || '#2A2A2A').toString()
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M3.84198 8.00234C3.84198 8.22335 3.92978 8.43531 4.08606 8.5916C4.24234 8.74788 4.4543 8.83567 4.67531 8.83567C4.89633 8.83567 5.10829 8.74788 5.26457 8.5916C5.42085 8.43531 5.50865 8.22335 5.50865 8.00234C5.50865 7.78133 5.42085 7.56936 5.26457 7.41308C5.10829 7.2568 4.89633 7.16901 4.67531 7.16901C4.4543 7.16901 4.24234 7.2568 4.08606 7.41308C3.92978 7.56936 3.84198 7.78133 3.84198 8.00234Z"
|
||||
fill="#D9D9D9"
|
||||
stroke="#D9D9D9"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.84198 13.0023C3.84198 13.2234 3.92978 13.4353 4.08606 13.5916C4.24234 13.7479 4.4543 13.8357 4.67531 13.8357C4.89633 13.8357 5.10829 13.7479 5.26457 13.5916C5.42085 13.4353 5.50865 13.2234 5.50865 13.0023C5.50865 12.7813 5.42085 12.5694 5.26457 12.4131C5.10829 12.2568 4.89633 12.169 4.67531 12.169C4.4543 12.169 4.24234 12.2568 4.08606 12.4131C3.92978 12.5694 3.84198 12.7813 3.84198 13.0023Z"
|
||||
fill="#D9D9D9"
|
||||
stroke="#D9D9D9"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.67535 8.00234C9.67535 8.22335 9.76315 8.43531 9.91943 8.5916C10.0757 8.74788 10.2877 8.83567 10.5087 8.83567C10.7297 8.83567 10.9417 8.74788 11.0979 8.5916C11.2542 8.43531 11.342 8.22335 11.342 8.00234C11.342 7.78133 11.2542 7.56936 11.0979 7.41308C10.9417 7.2568 10.7297 7.16901 10.5087 7.16901C10.2877 7.16901 10.0757 7.2568 9.91943 7.41308C9.76315 7.56936 9.67535 7.78133 9.67535 8.00234Z"
|
||||
fill="#D9D9D9"
|
||||
stroke="#D9D9D9"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.67535 13.0023C9.67535 13.2234 9.76315 13.4353 9.91943 13.5916C10.0757 13.7479 10.2877 13.8357 10.5087 13.8357C10.7297 13.8357 10.9417 13.7479 11.0979 13.5916C11.2542 13.4353 11.342 13.2234 11.342 13.0023C11.342 12.7813 11.2542 12.5694 11.0979 12.4131C10.9417 12.2568 10.7297 12.169 10.5087 12.169C10.2877 12.169 10.0757 12.2568 9.91943 12.4131C9.76315 12.5694 9.67535 12.7813 9.67535 13.0023Z"
|
||||
fill="#D9D9D9"
|
||||
stroke="#D9D9D9"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.5087 8.00234C15.5087 8.22335 15.5965 8.43531 15.7527 8.5916C15.909 8.74788 16.121 8.83567 16.342 8.83567C16.563 8.83567 16.775 8.74788 16.9313 8.5916C17.0875 8.43531 17.1753 8.22335 17.1753 8.00234C17.1753 7.78133 17.0875 7.56936 16.9313 7.41308C16.775 7.2568 16.563 7.16901 16.342 7.16901C16.121 7.16901 15.909 7.2568 15.7527 7.41308C15.5965 7.56936 15.5087 7.78133 15.5087 8.00234Z"
|
||||
fill="#D9D9D9"
|
||||
stroke="#D9D9D9"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.5087 13.0023C15.5087 13.2234 15.5965 13.4353 15.7527 13.5916C15.909 13.7479 16.121 13.8357 16.342 13.8357C16.563 13.8357 16.775 13.7479 16.9313 13.5916C17.0875 13.4353 17.1753 13.2234 17.1753 13.0023C17.1753 12.7813 17.0875 12.5694 16.9313 12.4131C16.775 12.2568 16.563 12.169 16.342 12.169C16.121 12.169 15.909 12.2568 15.7527 12.4131C15.5965 12.5694 15.5087 12.7813 15.5087 13.0023Z"
|
||||
fill="#D9D9D9"
|
||||
stroke="#D9D9D9"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,20 +22,20 @@ export class FollowingIcon extends React.Component<IconProps> {
|
|||
d="M12.5 4.5L4.5 8.5L12.5 12.5L20.5 8.5L12.5 4.5Z"
|
||||
fill={color}
|
||||
stroke={color}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.5 12.5L12.5 16.5L20.5 12.5"
|
||||
stroke={color}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.5 16.5L12.5 20.5L20.5 16.5"
|
||||
stroke={color}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -4,38 +4,48 @@ import { IconProps } from './IconProps'
|
|||
|
||||
import React from 'react'
|
||||
|
||||
export class FollowingIcon extends React.Component<IconProps> {
|
||||
export class HighlightsIcon extends React.Component<IconProps> {
|
||||
render() {
|
||||
const size = (this.props.size || 26).toString()
|
||||
const color = (this.props.color || '#2A2A2A').toString()
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 25 25"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M12.5 4.5L4.5 8.5L12.5 12.5L20.5 8.5L12.5 4.5Z"
|
||||
fill={color}
|
||||
stroke={color}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.74023 18.8567H7.57357L17.6361 8.79423C18.1444 8.2859 18.43 7.59646 18.43 6.87757C18.43 6.15868 18.1444 5.46923 17.6361 4.9609C17.1277 4.45257 16.4383 4.16699 15.7194 4.16699C15.0005 4.16699 14.3111 4.45257 13.8027 4.9609L3.74023 15.0234V18.8567Z"
|
||||
fill="#FFD234"
|
||||
stroke="#FFD234"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.5 12.5L12.5 16.5L20.5 12.5"
|
||||
stroke={color}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12.8447 5.91931L16.6781 9.75265"
|
||||
stroke="#FFD234"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.5 16.5L12.5 20.5L20.5 16.5"
|
||||
stroke={color}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5.17773 13.5859L9.01107 17.4193"
|
||||
stroke="#FFD234"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M20.9899 15.0234V18.8568H13.3232L17.1566 15.0234H20.9899Z"
|
||||
stroke="#FFD234"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
29
packages/web/components/elements/icons/HomeIcon.tsx
Normal file
29
packages/web/components/elements/icons/HomeIcon.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* eslint-disable functional/no-class */
|
||||
/* eslint-disable functional/no-this-expression */
|
||||
import { IconProps } from './IconProps'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export class HomeIcon extends React.Component<IconProps> {
|
||||
render() {
|
||||
const size = (this.props.size || 26).toString()
|
||||
const color = (this.props.color || '#2A2A2A').toString()
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 26 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M19.9329 12.3418L12.7458 6.07813C12.3924 5.77033 11.8662 5.77093 11.514 6.07933L4.35753 12.3433C3.70713 12.9127 4.11003 13.984 4.97433 13.984C5.49123 13.984 5.91033 14.4031 5.91033 14.9203V20.3893C5.91033 20.9065 6.32973 21.3253 6.84663 21.3253H10.1271V16.5322C10.1271 16.2397 10.3638 16.003 10.656 16.003H13.6359C13.9281 16.003 14.1651 16.2397 14.1651 16.5322V21.3256H17.2806C17.7978 21.3256 18.2169 20.9068 18.2169 20.3896V14.9206C18.2169 14.4034 18.636 13.9843 19.1529 13.9843H19.3179C20.1831 13.9837 20.5854 12.9103 19.9329 12.3418Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,6 @@ import React from 'react'
|
|||
|
||||
export class NewsletterFlairIcon extends React.Component<IconProps> {
|
||||
render() {
|
||||
const size = (this.props.size || 26).toString()
|
||||
const color = (this.props.color || '#2A2A2A').toString()
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="17"
|
||||
|
|
|
|||
32
packages/web/components/elements/icons/NewsletterIcon.tsx
Normal file
32
packages/web/components/elements/icons/NewsletterIcon.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* eslint-disable functional/no-class */
|
||||
/* eslint-disable functional/no-this-expression */
|
||||
import { IconProps } from './IconProps'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export class NewsletterIcon extends React.Component<IconProps> {
|
||||
render() {
|
||||
const size = (this.props.size || 20).toString()
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M14.6666 5.02332V11.3333C14.6666 11.8435 14.4717 12.3343 14.1217 12.7055C13.7718 13.0767 13.2932 13.3001 12.7839 13.33L12.6666 13.3333H3.33325C2.82311 13.3333 2.33224 13.1384 1.96108 12.7885C1.58991 12.4385 1.36651 11.9599 1.33659 11.4506L1.33325 11.3333V5.02332L7.62992 9.22132L7.70725 9.26532C7.79839 9.30984 7.89849 9.33299 7.99992 9.33299C8.10135 9.33299 8.20145 9.30984 8.29259 9.26532L8.36992 9.22132L14.6666 5.02332Z"
|
||||
fill="#007AFF"
|
||||
/>
|
||||
<path
|
||||
d="M12.6665 2.66669C13.3865 2.66669 14.0179 3.04669 14.3699 3.61802L7.99988 7.86469L1.62988 3.61802C1.79704 3.34653 2.02669 3.11895 2.29969 2.95428C2.57268 2.7896 2.8811 2.69259 3.19922 2.67135L3.33322 2.66669H12.6665Z"
|
||||
fill="#007AFF"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
|||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import type { LinkedItemCardProps } from './CardTypes'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
AuthorInfoStyle,
|
||||
CardCheckbox,
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import { HStack } from '../elements/LayoutPrimitives'
|
||||
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { DEFAULT_HEADER_HEIGHT } from '../templates/homeFeed/HeaderSpacer'
|
||||
import { LogoBox } from '../elements/LogoBox'
|
||||
|
||||
type HeaderProps = {
|
||||
user?: UserBasicData
|
||||
}
|
||||
|
||||
export function SettingsHeader(props: HeaderProps): JSX.Element {
|
||||
return (
|
||||
<nav>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
pr: '25px',
|
||||
height: DEFAULT_HEADER_HEIGHT,
|
||||
'@mdDown': {
|
||||
pr: '15px',
|
||||
},
|
||||
bg: '$thBackground',
|
||||
}}
|
||||
>
|
||||
<LogoBox />
|
||||
</HStack>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
@ -218,19 +218,14 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
|||
onSelect={() => headerDropdownActionHandler('navigate-to-rules')}
|
||||
title="Rules"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-api')}
|
||||
title="API Keys"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() =>
|
||||
headerDropdownActionHandler('navigate-to-integrations')
|
||||
}
|
||||
title="Integrations"
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-api')}
|
||||
title="API Keys"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-integrations')}
|
||||
title="Integrations"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => window.open('https://docs.omnivore.app', '_blank')}
|
||||
title="Documentation"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
|
||||
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { SettingsHeader } from '../patterns/SettingsHeader'
|
||||
import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useKeyboardShortcuts } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { useRouter } from 'next/router'
|
||||
|
|
@ -47,7 +46,6 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
|
|||
css={{ width: '100%', height: '100%', minHeight: '100vh' }}
|
||||
>
|
||||
<PageMetaData path="settings" title="Settings" />
|
||||
<SettingsHeader user={viewerData?.me} />
|
||||
<VStack css={{ width: '100%', height: '100%' }}>
|
||||
<Box
|
||||
css={{
|
||||
|
|
|
|||
|
|
@ -435,7 +435,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
key={label.id}
|
||||
text={label.name}
|
||||
color={label.color}
|
||||
useAppAppearance={props.isAppleAppEmbed}
|
||||
/>
|
||||
))}
|
||||
</SpanBox>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type { Highlight } from '../../../lib/networking/fragments/highlightFragm
|
|||
import {
|
||||
getHighlightElements,
|
||||
highlightIdAttribute,
|
||||
highlightLabelIdAttribute,
|
||||
highlightNoteIdAttribute,
|
||||
SelectionAttributes,
|
||||
} from '../../../lib/highlights/highlightHelpers'
|
||||
|
|
@ -79,15 +80,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 })
|
||||
|
||||
const [currentHighlightIdx, setCurrentHighlightIdx] = useState(0)
|
||||
const [focusedHighlight, setFocusedHighlight] = useState<
|
||||
Highlight | undefined
|
||||
>(undefined)
|
||||
const [focusedHighlight, setFocusedHighlight] =
|
||||
useState<Highlight | undefined>(undefined)
|
||||
|
||||
const [selectionData, setSelectionData] = useSelection(highlightLocations)
|
||||
|
||||
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [labelsTarget, setLabelsTarget] =
|
||||
useState<Highlight | undefined>(undefined)
|
||||
|
||||
const [
|
||||
confirmDeleteHighlightWithNoteId,
|
||||
|
|
@ -388,6 +387,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
highlight: highlight,
|
||||
highlightModalAction: 'addComment',
|
||||
})
|
||||
} else if ((target as Element).hasAttribute(highlightLabelIdAttribute)) {
|
||||
const id = (target as HTMLSpanElement).getAttribute(
|
||||
highlightLabelIdAttribute
|
||||
)
|
||||
const highlight = highlights.find(($0) => $0.id === id)
|
||||
setFocusedHighlight(highlight)
|
||||
setLabelsTarget(highlight)
|
||||
} else {
|
||||
window?.webkit?.messageHandlers.viewerAction?.postMessage({
|
||||
actionID: 'pageTapped',
|
||||
|
|
@ -395,7 +401,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
setFocusedHighlight(undefined)
|
||||
}
|
||||
},
|
||||
[openNoteModal, highlights]
|
||||
[openNoteModal, highlights, setLabelsTarget]
|
||||
)
|
||||
|
||||
const handleDoubleClick = useCallback(
|
||||
|
|
|
|||
|
|
@ -20,23 +20,24 @@ import {
|
|||
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
|
||||
import { NotebookContent } from '../article/Notebook'
|
||||
import { EmptyHighlights } from './EmptyHighlights'
|
||||
import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer'
|
||||
import { highlightsAsMarkdown } from './HighlightItem'
|
||||
import { MenuHeaderButton } from './LibraryHeader'
|
||||
|
||||
type HighlightItemsLayoutProps = {
|
||||
items: LibraryItem[]
|
||||
viewer: UserBasicData | undefined
|
||||
|
||||
showFilterMenu: boolean
|
||||
setShowFilterMenu: (show: boolean) => void
|
||||
|
||||
gridContainerRef: React.RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
export function HighlightItemsLayout(
|
||||
props: HighlightItemsLayoutProps
|
||||
): JSX.Element {
|
||||
// const headerHeight = useGetHeaderHeight()
|
||||
const [currentItem, setCurrentItem] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [currentItem, setCurrentItem] =
|
||||
useState<LibraryItem | undefined>(undefined)
|
||||
|
||||
const listReducer = (
|
||||
state: LibraryItem[],
|
||||
|
|
@ -106,10 +107,7 @@ export function HighlightItemsLayout(
|
|||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
|
||||
'@mdDown': {
|
||||
height: DEFAULT_HEADER_HEIGHT,
|
||||
},
|
||||
height: `100vh`,
|
||||
}}
|
||||
>
|
||||
<EmptyHighlights />
|
||||
|
|
@ -122,15 +120,12 @@ export function HighlightItemsLayout(
|
|||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
|
||||
height: `100vh`,
|
||||
'@lgDown': {
|
||||
overflowY: 'scroll',
|
||||
},
|
||||
bg: '$thBackground2',
|
||||
overflow: 'hidden',
|
||||
'@mdDown': {
|
||||
height: DEFAULT_HEADER_HEIGHT,
|
||||
},
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
|
|
@ -153,7 +148,7 @@ export function HighlightItemsLayout(
|
|||
>
|
||||
<VStack
|
||||
css={{
|
||||
minHeight: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
|
||||
minHeight: `100vh`,
|
||||
bg: '$thBackground',
|
||||
}}
|
||||
distribution="start"
|
||||
|
|
@ -167,8 +162,22 @@ export function HighlightItemsLayout(
|
|||
borderBottom: '1px solid $thBorderColor',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
></HStack>
|
||||
distribution="start"
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'none',
|
||||
'@mdDown': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuHeaderButton
|
||||
showFilterMenu={props.showFilterMenu}
|
||||
setShowFilterMenu={props.setShowFilterMenu}
|
||||
/>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
<LibraryItemsList
|
||||
items={items}
|
||||
viewer={props.viewer}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { EditLibraryItemModal } from './EditItemModals'
|
|||
import { EmptyLibrary } from './EmptyLibrary'
|
||||
import { HighlightItemsLayout } from './HighlightsLayout'
|
||||
import { LibraryFilterMenu } from '../navMenu/LibraryMenu'
|
||||
import { LibraryLegacyMenu } from '../navMenu/LibraryLegacyMenu'
|
||||
import { LibraryHeader, MultiSelectMode } from './LibraryHeader'
|
||||
import { UploadModal } from '../UploadModal'
|
||||
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
|
|
@ -68,7 +69,7 @@ const debouncedFetchSearchResults = debounce((query, cb) => {
|
|||
// We set a relatively high delay for the refresh at the end, as it's likely there's an issue
|
||||
// in processing. We give it the best attempt to be able to resolve, but if it doesn't we set
|
||||
// the state as Failed. On refresh it will try again if the backend sends "PROCESSING"
|
||||
const TIMEOUT_DELAYS = [1000, 2000, 2500, 3500, 5000, 10000, 60000]
|
||||
const TIMEOUT_DELAYS = [2000, 3500, 5000]
|
||||
|
||||
export function HomeFeedContainer(): JSX.Element {
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
|
|
@ -85,11 +86,13 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
|
||||
const gridContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [labelsTarget, setLabelsTarget] =
|
||||
useState<LibraryItem | undefined>(undefined)
|
||||
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [notebookTarget, setNotebookTarget] =
|
||||
useState<LibraryItem | undefined>(undefined)
|
||||
const [notebookTarget, setNotebookTarget] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
|
||||
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
|
||||
|
|
@ -206,8 +209,13 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
let startIdx = 0
|
||||
|
||||
const seeIfUpdated = async () => {
|
||||
if (startIdx > TIMEOUT_DELAYS.length) {
|
||||
if (startIdx >= TIMEOUT_DELAYS.length) {
|
||||
item.node.state = State.FAILED
|
||||
const updatedArticle = { ...item }
|
||||
updatedArticle.node = { ...item.node }
|
||||
updatedArticle.isLoading = false
|
||||
console.log(`Updating Metadata of ${item.node.slug}.`)
|
||||
performActionOnItem('update-item', updatedArticle)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -931,6 +939,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
key: 'libraryLayout',
|
||||
initialValue: 'LIST_LAYOUT',
|
||||
})
|
||||
const [navMenuStyle] = usePersistedState<'legacy' | 'shortcuts'>({
|
||||
key: 'library-nav-menu-style',
|
||||
initialValue: 'legacy',
|
||||
})
|
||||
|
||||
const updateLayout = useCallback(
|
||||
async (newLayout: LayoutType) => {
|
||||
|
|
@ -967,21 +979,35 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
)}
|
||||
|
||||
<HStack css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryFilterMenu
|
||||
setShowAddLinkModal={props.setShowAddLinkModal}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
|
||||
{navMenuStyle == 'shortcuts' && (
|
||||
<LibraryFilterMenu
|
||||
setShowAddLinkModal={props.setShowAddLinkModal}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
)}
|
||||
{navMenuStyle == 'legacy' && (
|
||||
<LibraryLegacyMenu
|
||||
setShowAddLinkModal={props.setShowAddLinkModal}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
)}
|
||||
{!props.isValidating && props.mode == 'highlights' && (
|
||||
<HighlightItemsLayout
|
||||
gridContainerRef={props.gridContainerRef}
|
||||
items={props.items}
|
||||
viewer={viewerData?.me}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ export const headerControlWidths = (
|
|||
return {
|
||||
width: '95%',
|
||||
'@mdDown': {
|
||||
width: multiSelectMode !== 'off' ? '100%' : '95%',
|
||||
padding: '15px',
|
||||
width: '100%',
|
||||
},
|
||||
'@media (min-width: 930px)': {
|
||||
width: '620px',
|
||||
|
|
|
|||
637
packages/web/components/templates/navMenu/LibraryLegacyMenu.tsx
Normal file
637
packages/web/components/templates/navMenu/LibraryLegacyMenu.tsx
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
import { ReactNode, useEffect, useMemo, useRef } from 'react'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Circle, X } from 'phosphor-react'
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionType,
|
||||
useGetSubscriptionsQuery,
|
||||
} from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { LogoBox } from '../../elements/LogoBox'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { useGetSavedSearchQuery } from '../../../lib/networking/queries/useGetSavedSearchQuery'
|
||||
import { SavedSearch } from '../../../lib/networking/fragments/savedSearchFragment'
|
||||
import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
|
||||
import Link from 'next/link'
|
||||
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
|
||||
import { NavMenuFooter } from './Footer'
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
|
||||
|
||||
type LibraryFilterMenuProps = {
|
||||
setShowAddLinkModal: (show: boolean) => void
|
||||
|
||||
searchTerm: string | undefined
|
||||
applySearchQuery: (searchTerm: string) => void
|
||||
|
||||
showFilterMenu: boolean
|
||||
setShowFilterMenu: (show: boolean) => void
|
||||
}
|
||||
|
||||
export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
|
||||
const [labels, setLabels] = usePersistedState<Label[]>({
|
||||
key: 'menu-labels',
|
||||
isSessionStorage: false,
|
||||
initialValue: [],
|
||||
})
|
||||
const [savedSearches, setSavedSearches] = usePersistedState<SavedSearch[]>({
|
||||
key: 'menu-searches',
|
||||
isSessionStorage: false,
|
||||
initialValue: [],
|
||||
})
|
||||
const [subscriptions, setSubscriptions] = usePersistedState<Subscription[]>({
|
||||
key: 'menu-subscriptions',
|
||||
isSessionStorage: false,
|
||||
initialValue: [],
|
||||
})
|
||||
const labelsResponse = useGetLabelsQuery()
|
||||
const searchesResponse = useGetSavedSearchQuery()
|
||||
const subscriptionsResponse = useGetSubscriptionsQuery()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!labelsResponse.error &&
|
||||
!labelsResponse.isLoading &&
|
||||
labelsResponse.labels
|
||||
) {
|
||||
setLabels(labelsResponse.labels)
|
||||
}
|
||||
}, [setLabels, labelsResponse])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!subscriptionsResponse.error &&
|
||||
!subscriptionsResponse.isLoading &&
|
||||
subscriptionsResponse.subscriptions
|
||||
) {
|
||||
setSubscriptions(subscriptionsResponse.subscriptions)
|
||||
}
|
||||
}, [setSubscriptions, subscriptionsResponse])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!searchesResponse.error &&
|
||||
!searchesResponse.isLoading &&
|
||||
searchesResponse.savedSearches
|
||||
) {
|
||||
setSavedSearches(searchesResponse.savedSearches)
|
||||
}
|
||||
}, [setSavedSearches, searchesResponse])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
position: 'fixed',
|
||||
bg: '$thLeftMenuBackground',
|
||||
height: '100%',
|
||||
width: LIBRARY_LEFT_MENU_WIDTH,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
'@mdDown': {
|
||||
width: '100%',
|
||||
transition: 'top 100ms, visibility 100ms',
|
||||
top: props.showFilterMenu ? '0' : '100%',
|
||||
visibility: props.showFilterMenu ? 'visible' : 'hidden',
|
||||
},
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
px: '25px',
|
||||
pb: '17px',
|
||||
pt: '4.5px',
|
||||
lineHeight: '1',
|
||||
'@mdDown': {
|
||||
pb: '0px',
|
||||
pt: '5px',
|
||||
px: '15px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SpanBox css={{ '@mdDown': { display: 'none' } }}>
|
||||
<LogoBox />
|
||||
</SpanBox>
|
||||
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@mdDown': {
|
||||
display: 'flex',
|
||||
ml: 'auto',
|
||||
pt: '10px',
|
||||
pb: '0px',
|
||||
},
|
||||
}}
|
||||
onClick={(event) => {
|
||||
props.setShowFilterMenu(false)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<X size={30} />
|
||||
</Button>
|
||||
</Box>
|
||||
<SavedSearches {...props} savedSearches={savedSearches} />
|
||||
<Subscriptions {...props} subscriptions={subscriptions} />
|
||||
<Labels {...props} labels={labels} />
|
||||
<NavMenuFooter {...props} />
|
||||
<Box css={{ height: '250px ' }} />
|
||||
</Box>
|
||||
{/* This spacer pushes library content to the right of
|
||||
the fixed left side menu. */}
|
||||
<Box
|
||||
css={{
|
||||
minWidth: LIBRARY_LEFT_MENU_WIDTH,
|
||||
height: '100%',
|
||||
bg: '$thBackground',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
></Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SavedSearches(
|
||||
props: LibraryFilterMenuProps & { savedSearches: SavedSearch[] | undefined }
|
||||
): JSX.Element {
|
||||
const sortedSearches = useMemo(() => {
|
||||
return props.savedSearches
|
||||
?.filter((it) => it.visible)
|
||||
?.sort(
|
||||
(left: SavedSearch, right: SavedSearch) =>
|
||||
left.position - right.position
|
||||
)
|
||||
}, [props.savedSearches])
|
||||
|
||||
useRegisterActions(
|
||||
(sortedSearches ?? []).map((item, idx) => {
|
||||
const key = String(idx + 1)
|
||||
return {
|
||||
id: `saved_search_${key}`,
|
||||
name: item.name,
|
||||
shortcut: [key],
|
||||
section: 'Saved Searches',
|
||||
keywords: '?' + item.name,
|
||||
perform: () => {
|
||||
props.applySearchQuery(item.filter)
|
||||
},
|
||||
}
|
||||
}),
|
||||
[props.savedSearches]
|
||||
)
|
||||
|
||||
const [collapsed, setCollapsed] = usePersistedState<boolean>({
|
||||
key: `--saved-searches-collapsed`,
|
||||
initialValue: false,
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuPanel
|
||||
title="Saved Searches"
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
>
|
||||
{!collapsed &&
|
||||
sortedSearches &&
|
||||
sortedSearches?.map((item) => (
|
||||
<FilterButton
|
||||
key={item.name}
|
||||
text={item.name}
|
||||
filterTerm={item.filter}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
{!collapsed && sortedSearches !== undefined && (
|
||||
<EditButton
|
||||
title="Edit Saved Searches"
|
||||
destination="/settings/saved-searches"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box css={{ height: '10px' }}></Box>
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function Subscriptions(
|
||||
props: LibraryFilterMenuProps & { subscriptions: Subscription[] | undefined }
|
||||
): JSX.Element {
|
||||
const [collapsed, setCollapsed] = usePersistedState<boolean>({
|
||||
key: `--subscriptions-collapsed`,
|
||||
initialValue: false,
|
||||
})
|
||||
|
||||
const sortedSubscriptions = useMemo(() => {
|
||||
if (!props.subscriptions) {
|
||||
return []
|
||||
}
|
||||
return props.subscriptions
|
||||
.filter((s) => s.status == 'ACTIVE')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [props.subscriptions])
|
||||
|
||||
useRegisterActions(
|
||||
(sortedSubscriptions ?? []).map((subscription, idx) => {
|
||||
const key = String(idx + 1)
|
||||
const name = subscription.name
|
||||
return {
|
||||
id: `subscription_${key}`,
|
||||
section: 'Subscriptions',
|
||||
name: name,
|
||||
keywords: '*' + name,
|
||||
perform: () => {
|
||||
props.applySearchQuery(`subscription:\"${name}\"`)
|
||||
},
|
||||
}
|
||||
}),
|
||||
[sortedSubscriptions]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenuPanel
|
||||
title="Subscriptions"
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
>
|
||||
{!collapsed ? (
|
||||
<>
|
||||
<FilterButton
|
||||
filterTerm="in:inbox has:subscriptions"
|
||||
text="All"
|
||||
{...props}
|
||||
/>
|
||||
<FilterButton
|
||||
filterTerm={`in:inbox label:RSS`}
|
||||
text="Feeds"
|
||||
{...props}
|
||||
/>
|
||||
<FilterButton
|
||||
filterTerm={`in:inbox label:Newsletter`}
|
||||
text="Newsletters"
|
||||
{...props}
|
||||
/>
|
||||
{(sortedSubscriptions ?? []).map((item) => {
|
||||
switch (item.type) {
|
||||
case SubscriptionType.NEWSLETTER:
|
||||
return (
|
||||
<FilterButton
|
||||
key={item.id}
|
||||
filterTerm={`in:inbox subscription:\"${item.name}\"`}
|
||||
text={item.name}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
case SubscriptionType.RSS:
|
||||
return (
|
||||
<FilterButton
|
||||
key={item.id}
|
||||
filterTerm={`in:inbox rss:\"${item.url}\"`}
|
||||
text={item.name}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
<EditButton
|
||||
title="Edit Subscriptions"
|
||||
destination="/settings/subscriptions"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<SpanBox css={{ mb: '10px' }} />
|
||||
)}
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function Labels(
|
||||
props: LibraryFilterMenuProps & { labels: Label[] }
|
||||
): JSX.Element {
|
||||
const [collapsed, setCollapsed] = usePersistedState<boolean>({
|
||||
key: `--labels-collapsed`,
|
||||
initialValue: false,
|
||||
})
|
||||
|
||||
const sortedLabels = useMemo(() => {
|
||||
return props.labels.sort((left: Label, right: Label) =>
|
||||
left.name.localeCompare(right.name)
|
||||
)
|
||||
}, [props.labels])
|
||||
|
||||
return (
|
||||
<MenuPanel
|
||||
title="Labels"
|
||||
editTitle="Edit Labels"
|
||||
hideBottomBorder={true}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
>
|
||||
{!collapsed && (
|
||||
<>
|
||||
{sortedLabels.map((item) => {
|
||||
return <LabelButton key={item.id} label={item} {...props} />
|
||||
})}
|
||||
<EditButton title="Edit Labels" destination="/settings/labels" />
|
||||
</>
|
||||
)}
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
type MenuPanelProps = {
|
||||
title: string
|
||||
children: ReactNode
|
||||
editFunc?: () => void
|
||||
editTitle?: string
|
||||
hideBottomBorder?: boolean
|
||||
collapsed: boolean
|
||||
setCollapsed: (collapsed: boolean) => void
|
||||
}
|
||||
|
||||
function MenuPanel(props: MenuPanelProps): JSX.Element {
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
m: '0px',
|
||||
width: '100%',
|
||||
borderBottom: props.hideBottomBorder
|
||||
? '1px solid transparent'
|
||||
: '1px solid $thBorderColor',
|
||||
px: '15px',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<HStack css={{ width: '100%' }} distribution="start" alignment="center">
|
||||
<StyledText
|
||||
css={{
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
lineHeight: '125%',
|
||||
color: '$thLibraryMenuPrimary',
|
||||
pl: '10px',
|
||||
mt: '20px',
|
||||
mb: '10px',
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</StyledText>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
mt: '10px',
|
||||
marginLeft: 'auto',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={(event) => {
|
||||
props.setCollapsed(!props.collapsed)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.collapsed ? (
|
||||
<ToggleCaretRightIcon
|
||||
size={15}
|
||||
color={theme.colors.thLibraryMenuPrimary.toString()}
|
||||
/>
|
||||
) : (
|
||||
<ToggleCaretDownIcon
|
||||
size={15}
|
||||
color={theme.colors.thLibraryMenuPrimary.toString()}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
{props.children}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type FilterButtonProps = {
|
||||
text: string
|
||||
|
||||
filterTerm: string
|
||||
searchTerm: string | undefined
|
||||
|
||||
applySearchQuery: (searchTerm: string) => void
|
||||
|
||||
setShowFilterMenu: (show: boolean) => void
|
||||
}
|
||||
|
||||
function FilterButton(props: FilterButtonProps): JSX.Element {
|
||||
const isInboxFilter = (filter: string) => {
|
||||
return filter === '' || filter === 'in:inbox'
|
||||
}
|
||||
const selected = useMemo(() => {
|
||||
if (isInboxFilter(props.filterTerm) && !props.searchTerm) {
|
||||
return true
|
||||
}
|
||||
return props.searchTerm === props.filterTerm
|
||||
}, [props.searchTerm, props.filterTerm])
|
||||
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
pl: '10px',
|
||||
mb: '2px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
height: '32px',
|
||||
|
||||
backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: selected
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
backgroundColor: selected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: selected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
title={props.text}
|
||||
onClick={(e) => {
|
||||
props.applySearchQuery(props.filterTerm)
|
||||
props.setShowFilterMenu(false)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type LabelButtonProps = {
|
||||
label: Label
|
||||
searchTerm: string | undefined
|
||||
applySearchQuery: (searchTerm: string) => void
|
||||
}
|
||||
|
||||
function LabelButton(props: LabelButtonProps): JSX.Element {
|
||||
const labelId = `checkbox-label-${props.label.id}`
|
||||
const checkboxRef = useRef<HTMLInputElement | null>(null)
|
||||
const state = useMemo(() => {
|
||||
const term = props.searchTerm ?? ''
|
||||
if (term.indexOf(`label:\"${props.label.name}\"`) >= 0) {
|
||||
return 'on'
|
||||
}
|
||||
return 'off'
|
||||
}, [props.searchTerm, props.label])
|
||||
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
pl: '10px',
|
||||
pt: '2px', // TODO: hack to middle align
|
||||
width: '100%',
|
||||
height: '30px',
|
||||
|
||||
fontSize: '14px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color:
|
||||
state == 'on'
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
|
||||
m: '0px',
|
||||
'&:hover': {
|
||||
backgroundColor: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
title={props.label.name}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
>
|
||||
<label
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
maxWidth: '170px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
onClick={() => {
|
||||
const query = props.searchTerm?.replace(/label:\"(.*)\"/, '') ?? ''
|
||||
if (checkboxRef.current?.checked) {
|
||||
props.applySearchQuery(query.trim())
|
||||
} else {
|
||||
props.applySearchQuery(
|
||||
`${query.trim()} label:\"${props.label.name}\"`
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Circle size={9} color={props.label.color} weight="fill" />
|
||||
<SpanBox css={{ pl: '10px' }}>{props.label.name}</SpanBox>
|
||||
</label>
|
||||
<SpanBox
|
||||
css={{
|
||||
ml: 'auto',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id={labelId}
|
||||
ref={checkboxRef}
|
||||
type="checkbox"
|
||||
checked={state === 'on'}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
props.applySearchQuery(
|
||||
`${props.searchTerm ?? ''} label:\"${props.label.name}\"`
|
||||
)
|
||||
} else {
|
||||
const query =
|
||||
props.searchTerm?.replace(
|
||||
`label:\"${props.label.name}\"`,
|
||||
''
|
||||
) ?? ''
|
||||
props.applySearchQuery(query)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type EditButtonProps = {
|
||||
title: string
|
||||
destination: string
|
||||
}
|
||||
|
||||
function EditButton(props: EditButtonProps): JSX.Element {
|
||||
return (
|
||||
<Link href={props.destination} passHref legacyBehavior>
|
||||
<SpanBox
|
||||
css={{
|
||||
ml: '10px',
|
||||
mb: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
height: '32px',
|
||||
|
||||
fontSize: '14px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: '$thLibraryMenuUnselected',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</SpanBox>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { ReactNode, useEffect, useMemo, useRef } from 'react'
|
|||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Circle } from 'phosphor-react'
|
||||
import { Circle, DotsThree, MagnifyingGlass, X } from 'phosphor-react'
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionType,
|
||||
|
|
@ -20,6 +20,19 @@ import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
|
|||
import Link from 'next/link'
|
||||
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
|
||||
import { NavMenuFooter } from './Footer'
|
||||
import { FollowingIcon } from '../../elements/icons/FollowingIcon'
|
||||
import { HomeIcon } from '../../elements/icons/HomeIcon'
|
||||
import { LibraryIcon } from '../../elements/icons/LibraryIcon'
|
||||
import { HighlightsIcon } from '../../elements/icons/HighlightsIcon'
|
||||
import { CoverImage } from '../../elements/CoverImage'
|
||||
import { Shortcut } from '../../../pages/settings/shortcuts'
|
||||
import { OutlinedLabelChip } from '../../elements/OutlinedLabelChip'
|
||||
import { NewsletterFlairIcon } from '../../elements/icons/NewsletterFlairIcon'
|
||||
import { FeedFlairIcon } from '../../elements/icons/FeedFlairIcon'
|
||||
import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
|
||||
import { DropdownMenu } from '@radix-ui/react-dropdown-menu'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
|
||||
|
||||
|
|
@ -99,27 +112,56 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
|
|||
display: 'none',
|
||||
},
|
||||
'@mdDown': {
|
||||
visibility: props.showFilterMenu ? 'visible' : 'hidden',
|
||||
width: '100%',
|
||||
transition: 'visibility 0s, top 150ms',
|
||||
transition: 'top 100ms, visibility 100ms',
|
||||
top: props.showFilterMenu ? '0' : '100%',
|
||||
visibility: props.showFilterMenu ? 'visible' : 'hidden',
|
||||
},
|
||||
zIndex: 3,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
px: '25px',
|
||||
pb: '25px',
|
||||
pb: '17px',
|
||||
pt: '4.5px',
|
||||
lineHeight: '1',
|
||||
'@mdDown': {
|
||||
pb: '0px',
|
||||
pt: '5px',
|
||||
px: '15px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoBox />
|
||||
<SpanBox css={{ '@mdDown': { display: 'none' } }}>
|
||||
<LogoBox />
|
||||
</SpanBox>
|
||||
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@mdDown': {
|
||||
display: 'flex',
|
||||
ml: 'auto',
|
||||
pt: '10px',
|
||||
pb: '0px',
|
||||
},
|
||||
}}
|
||||
onClick={(event) => {
|
||||
props.setShowFilterMenu(false)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<X size={30} />
|
||||
</Button>
|
||||
</Box>
|
||||
<SavedSearches {...props} savedSearches={savedSearches} />
|
||||
<LibraryNav {...props} />
|
||||
<Shortcuts {...props} />
|
||||
{/* <SavedSearches {...props} savedSearches={savedSearches} />
|
||||
<Subscriptions {...props} subscriptions={subscriptions} />
|
||||
<Labels {...props} labels={labels} />
|
||||
<Labels {...props} labels={labels} /> */}
|
||||
<NavMenuFooter {...props} />
|
||||
<Box css={{ height: '250px ' }} />
|
||||
</Box>
|
||||
|
|
@ -139,6 +181,277 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => {
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
m: '0px',
|
||||
mt: '10px',
|
||||
gap: '5px',
|
||||
width: '100%',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
px: '15px',
|
||||
pb: '25px',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<NavButton
|
||||
{...props}
|
||||
text="Home"
|
||||
filterTerm="in:library OR in:following use:folders"
|
||||
icon={<HomeIcon color={theme.colors.thHomeIcon.toString()} />}
|
||||
/>
|
||||
<NavButton
|
||||
{...props}
|
||||
text="Following"
|
||||
filterTerm="in:following use:folders"
|
||||
icon={<FollowingIcon color="#F59932" />}
|
||||
/>
|
||||
<NavButton
|
||||
{...props}
|
||||
text="Library"
|
||||
filterTerm="in:library use:folders"
|
||||
icon={<LibraryIcon color={theme.colors.ctaBlue.toString()} />}
|
||||
/>
|
||||
<NavButton
|
||||
{...props}
|
||||
text="Highlights"
|
||||
filterTerm="in:all has:highlights mode:highlights"
|
||||
icon={<HighlightsIcon color={theme.colors.highlight.toString()} />}
|
||||
/>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const Shortcuts = (props: LibraryFilterMenuProps): JSX.Element => {
|
||||
const router = useRouter()
|
||||
const [shortcuts] = usePersistedState<Shortcut[]>({
|
||||
key: 'library-shortcuts',
|
||||
isSessionStorage: false,
|
||||
initialValue: [],
|
||||
})
|
||||
|
||||
// const shortcuts: Shortcut[] = [
|
||||
// {
|
||||
// id: '12asdfasdf',
|
||||
// name: 'Omnivore Blog',
|
||||
// icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'subscription:"Money Talk"',
|
||||
// type: 'feed',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfsdfgdsfg',
|
||||
// name: 'Follow the Money | Arne & Harr',
|
||||
// filter: 'subscription:"Money Talk"',
|
||||
// type: 'feed',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfasdfasdfsdfsdfsgasdfg',
|
||||
// name: 'Andrew Kenneson from Center for the Study of Partisanship and Ideology',
|
||||
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'in:all label:"Hockey"',
|
||||
// type: 'newsletter',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfasdfasdfsdfsdfsgasdfg',
|
||||
// name: 'Robert的博客',
|
||||
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'in:all label:"Hockey"',
|
||||
// type: 'feed',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfasdfasdfasdfasf',
|
||||
// name: 'Oldest First',
|
||||
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'in:all label:"Hockey"',
|
||||
// type: 'search',
|
||||
// },
|
||||
// {
|
||||
// id: 'sdfasdfasdfgasdfg',
|
||||
// name: 'Hockey',
|
||||
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
|
||||
// filter: 'in:all label:"Hockey"',
|
||||
// type: 'label',
|
||||
// label: {
|
||||
// id: 'sdfsdfsdf',
|
||||
// name: 'Hockey',
|
||||
// color: '#E98B8B',
|
||||
// createdAt: new Date(),
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
//
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
m: '0px',
|
||||
gap: '8px',
|
||||
width: '100%',
|
||||
px: '15px',
|
||||
pb: '25px',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ width: '100%', pr: '0px' }}
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
fontFamily: '$display',
|
||||
fontSize: '14px',
|
||||
lineHeight: '125%',
|
||||
color: '$thLibraryMenuPrimary',
|
||||
pl: '10px',
|
||||
// mt: '20px',
|
||||
mb: '10px',
|
||||
}}
|
||||
>
|
||||
SHORTCUTS
|
||||
</StyledText>
|
||||
<SpanBox css={{ display: 'flex', ml: 'auto' }}>
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
triggerElement={<DotsThree size={20} />}
|
||||
css={{ ml: 'auto' }}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
router.push(`/settings/shortcuts`)
|
||||
}}
|
||||
title="Edit shortcuts"
|
||||
/>
|
||||
</Dropdown>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
{shortcuts.map((shortcut) => {
|
||||
const selected = props.searchTerm === shortcut.filter
|
||||
return (
|
||||
<Box
|
||||
key={`shortcut-${shortcut.id}`}
|
||||
css={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
height: '32px',
|
||||
|
||||
backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
|
||||
color: selected
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
backgroundColor: selected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: selected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
title={shortcut.name}
|
||||
onClick={(e) => {
|
||||
props.applySearchQuery(shortcut.filter)
|
||||
props.setShowFilterMenu(false)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
{(shortcut.type == 'feed' || shortcut.type == 'newsletter') && (
|
||||
<FeedOrNewsletterShortcut shortcut={shortcut} />
|
||||
)}
|
||||
{shortcut.type == 'search' && (
|
||||
<SearchShortcut shortcut={shortcut} />
|
||||
)}
|
||||
{shortcut.type == 'label' && <LabelShortcut shortcut={shortcut} />}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type ShortcutItemProps = {
|
||||
shortcut: Shortcut
|
||||
}
|
||||
|
||||
const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '10px', width: '100%', gap: '10px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ minWidth: '20px' }}
|
||||
>
|
||||
{props.shortcut.icon ? (
|
||||
<CoverImage
|
||||
src={props.shortcut.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
css={{ borderRadius: '20px' }}
|
||||
/>
|
||||
) : props.shortcut.type == 'newsletter' ? (
|
||||
<NewsletterIcon color="#F59932" size={18} />
|
||||
) : (
|
||||
<FollowingIcon color="#F59932" size={21} />
|
||||
)}
|
||||
</HStack>
|
||||
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '10px', width: '100%', gap: '10px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ minWidth: '20px' }}
|
||||
>
|
||||
<MagnifyingGlass size={18} />
|
||||
</HStack>
|
||||
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const LabelShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '5px', width: '100%' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<OutlinedLabelChip
|
||||
text={props.shortcut.name}
|
||||
color={props.shortcut.label?.color ?? 'gray'}
|
||||
/>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
function SavedSearches(
|
||||
props: LibraryFilterMenuProps & { savedSearches: SavedSearch[] | undefined }
|
||||
): JSX.Element {
|
||||
|
|
@ -175,7 +488,7 @@ function SavedSearches(
|
|||
|
||||
return (
|
||||
<MenuPanel
|
||||
title="Saved Searches"
|
||||
title="SHORTCUTS"
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
>
|
||||
|
|
@ -353,9 +666,8 @@ function MenuPanel(props: MenuPanelProps): JSX.Element {
|
|||
<HStack css={{ width: '100%' }} distribution="start" alignment="center">
|
||||
<StyledText
|
||||
css={{
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
fontFamily: '$display',
|
||||
fontSize: '14px',
|
||||
lineHeight: '125%',
|
||||
color: '$thLibraryMenuPrimary',
|
||||
pl: '10px',
|
||||
|
|
@ -400,6 +712,78 @@ function MenuPanel(props: MenuPanelProps): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
type NavButtonProps = {
|
||||
text: string
|
||||
icon: ReactNode
|
||||
|
||||
filterTerm: string
|
||||
searchTerm: string | undefined
|
||||
|
||||
applySearchQuery: (searchTerm: string) => void
|
||||
setShowFilterMenu: (show: boolean) => void
|
||||
}
|
||||
|
||||
function NavButton(props: NavButtonProps): JSX.Element {
|
||||
const isInboxFilter = (filter: string) => {
|
||||
return filter === '' || filter === 'in:inbox'
|
||||
}
|
||||
const selected = useMemo(() => {
|
||||
if (isInboxFilter(props.filterTerm) && !props.searchTerm) {
|
||||
return true
|
||||
}
|
||||
return props.searchTerm === props.filterTerm
|
||||
}, [props.searchTerm, props.filterTerm])
|
||||
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
pl: '10px',
|
||||
mb: '2px',
|
||||
gap: '10px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
height: '34px',
|
||||
|
||||
backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
|
||||
fontSize: '15px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: selected
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
backgroundColor: selected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: selected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
title={props.text}
|
||||
onClick={(e) => {
|
||||
props.applySearchQuery(props.filterTerm)
|
||||
props.setShowFilterMenu(false)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.icon}
|
||||
{props.text}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type FilterButtonProps = {
|
||||
text: string
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ import { NavMenuFooter } from './Footer'
|
|||
const HorizontalDivider = styled(SpanBox, {
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
my: '25px',
|
||||
background: `${theme.colors.grayLine.toString()}`,
|
||||
my: '5px',
|
||||
opacity: '0.1',
|
||||
background: `${theme.colors.thTextSubtle3.toString()}`,
|
||||
})
|
||||
|
||||
const StyledLink = styled(SpanBox, {
|
||||
|
|
@ -87,6 +88,7 @@ export function SettingsMenu(): JSX.Element {
|
|||
{ name: 'Feeds', destination: '/settings/feeds' },
|
||||
{ name: 'Subscriptions', destination: '/settings/subscriptions' },
|
||||
{ name: 'Labels', destination: '/settings/labels' },
|
||||
{ name: 'Shortcuts', destination: '/settings/shortcuts' },
|
||||
{ name: 'Saved Searches', destination: '/settings/saved-searches' },
|
||||
{ name: 'Pinned Searches', destination: '/settings/pinned-searches' },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,12 +2,38 @@ import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
|||
import { Button } from '../../elements/Button'
|
||||
import { PrimaryDropdown } from '../PrimaryDropdown'
|
||||
import { LogoBox } from '../../elements/LogoBox'
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { DEFAULT_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { ReaderSettingsIcon } from '../../elements/icons/ReaderSettingsIcon'
|
||||
import { CircleUtilityMenuIcon } from '../../elements/icons/CircleUtilityMenuIcon'
|
||||
|
||||
function useScrollDirection() {
|
||||
const [scrollDirection, setScrollDirection] = useState('up')
|
||||
|
||||
useEffect(() => {
|
||||
let lastScrollY = window.pageYOffset
|
||||
|
||||
const updateScrollDirection = () => {
|
||||
const scrollY = window.pageYOffset
|
||||
const direction = scrollY > lastScrollY ? 'down' : 'up'
|
||||
if (
|
||||
direction !== scrollDirection &&
|
||||
(scrollY - lastScrollY > 10 || scrollY - lastScrollY < -10)
|
||||
) {
|
||||
setScrollDirection(direction)
|
||||
}
|
||||
lastScrollY = scrollY > 0 ? scrollY : 0
|
||||
}
|
||||
window.addEventListener('scroll', updateScrollDirection) // add event listener
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateScrollDirection) // clean up
|
||||
}
|
||||
}, [scrollDirection])
|
||||
|
||||
return scrollDirection
|
||||
}
|
||||
|
||||
type ReaderHeaderProps = {
|
||||
alwaysDisplayToolbar: boolean
|
||||
hideDisplaySettings: boolean
|
||||
|
|
@ -16,6 +42,8 @@ type ReaderHeaderProps = {
|
|||
}
|
||||
|
||||
export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
|
||||
const scrollDirection = useScrollDirection()
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack
|
||||
|
|
@ -26,6 +54,7 @@ export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
|
|||
left: '0',
|
||||
zIndex: 1,
|
||||
pt: '0px',
|
||||
pb: '15px',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
height: DEFAULT_HEADER_HEIGHT,
|
||||
|
|
@ -37,8 +66,12 @@ export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
|
|||
pointerEvents: 'unset',
|
||||
},
|
||||
'@mdDown': {
|
||||
top: scrollDirection === 'down' ? '-90px' : '0',
|
||||
bg: '$readerBg',
|
||||
pointerEvents: 'unset',
|
||||
transitionProperty: 'top',
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '200ms',
|
||||
},
|
||||
'@media print': {
|
||||
display: 'none',
|
||||
|
|
|
|||
|
|
@ -181,11 +181,18 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
|||
thLibrarySearchbox: '#FCFCFC',
|
||||
thLibraryMenuPrimary: '#3D3D3D',
|
||||
thLibraryMenuSecondary: '#3D3D3D',
|
||||
thLibraryMenuUnselected: '#898989',
|
||||
thLibraryMenuUnselected: '#3D3D3D',
|
||||
thLibrarySelectionColor: '#FFEA9F',
|
||||
thLibraryNavigationMenuFooter: '#EFEADE',
|
||||
thLibraryMenuFooterHover: '#FFFFFF',
|
||||
thFormInput: '#EBEBEB',
|
||||
thHomeIcon: '#2A2A2A',
|
||||
|
||||
thLabelChipForeground: '#2A2A2A', // : '#2A2A2A'
|
||||
thLabelChipBackground: '#F5F5F5',
|
||||
thLabelChipSelectedBorder: 'black',
|
||||
thLabelChipUnselectedBorder: '#F5F5F5',
|
||||
thLabelOutlineChipBorder: '#D9D9D9',
|
||||
|
||||
thHeaderIconRing: '#D9D9D9',
|
||||
thHeaderIconInner: '#898989',
|
||||
|
|
@ -305,12 +312,19 @@ const darkThemeSpec = {
|
|||
thLibrarySearchbox: '#3D3D3D',
|
||||
thLibraryMenuPrimary: '#EBEBEB',
|
||||
thLibraryMenuSecondary: '#EBEBEB',
|
||||
thLibraryMenuUnselected: '#898989',
|
||||
thLibrarySelectionColor: '#3D3D3D',
|
||||
thLibraryMenuUnselected: 'white',
|
||||
thLibrarySelectionColor: '#6A6968',
|
||||
thLibraryNavigationMenuFooter: '#3D3D3D',
|
||||
thLibraryMenuFooterHover: '#6A6968',
|
||||
searchActiveOutline: '#866D15',
|
||||
thFormInput: '#3D3D3D',
|
||||
thHomeIcon: '#FFFFFF',
|
||||
|
||||
thLabelChipForeground: '#EBEBEB',
|
||||
thLabelChipBackground: '#2A2A2A',
|
||||
thLabelChipSelectedBorder: '#FFEA9F',
|
||||
thLabelChipUnselectedBorder: '#2A2A2A',
|
||||
thLabelOutlineChipBorder: '#6A696850',
|
||||
|
||||
thHeaderIconRing: '#3D3D3D',
|
||||
thHeaderIconInner: '#D9D9D9',
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export async function createHighlight(
|
|||
patch,
|
||||
id,
|
||||
annotations.length > 0,
|
||||
false,
|
||||
input.color
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import type { Highlight } from '../networking/fragments/highlightFragment'
|
|||
import { interpolationSearch } from './interpolationSearch'
|
||||
import {
|
||||
highlightIdAttribute,
|
||||
highlightLabelIdAttribute,
|
||||
highlightNoteIdAttribute,
|
||||
labelsImage,
|
||||
noteImage,
|
||||
} from './highlightHelpers'
|
||||
|
||||
|
|
@ -80,10 +82,18 @@ function nodeAttributesFromHighlight(
|
|||
const patch = highlight.patch
|
||||
const id = highlight.id
|
||||
const withNote = !!highlight.annotation
|
||||
const withLabels = (highlight.labels?.length ?? 0) > 0
|
||||
const tooltip = undefined
|
||||
const customColor = highlight.color
|
||||
|
||||
return makeHighlightNodeAttributes(patch, id, withNote, customColor, tooltip)
|
||||
return makeHighlightNodeAttributes(
|
||||
patch,
|
||||
id,
|
||||
withNote,
|
||||
withLabels,
|
||||
customColor,
|
||||
tooltip
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,6 +110,7 @@ export function makeHighlightNodeAttributes(
|
|||
patch: string,
|
||||
id: string,
|
||||
withNote: boolean,
|
||||
withLabels: boolean,
|
||||
customColor?: string,
|
||||
tooltip?: string
|
||||
): HighlightNodeAttributes {
|
||||
|
|
@ -183,12 +194,10 @@ export function makeHighlightNodeAttributes(
|
|||
startingTextNodeIndex++
|
||||
}
|
||||
if (withNote && lastElement) {
|
||||
lastElement.classList.add('last_element')
|
||||
|
||||
const svg = noteImage(customColor)
|
||||
svg.setAttribute(highlightNoteIdAttribute, id)
|
||||
|
||||
const ctr = document.createElement('div')
|
||||
const ctr = document.createElement('span')
|
||||
ctr.className = 'highlight_note_button'
|
||||
ctr.appendChild(svg)
|
||||
ctr.setAttribute(highlightNoteIdAttribute, id)
|
||||
|
|
@ -197,6 +206,19 @@ export function makeHighlightNodeAttributes(
|
|||
|
||||
lastElement.appendChild(ctr)
|
||||
}
|
||||
if (withLabels && lastElement) {
|
||||
const svg = labelsImage(customColor)
|
||||
svg.setAttribute(highlightLabelIdAttribute, id)
|
||||
|
||||
const ctr = document.createElement('span')
|
||||
ctr.className = 'highlight_label_button'
|
||||
ctr.appendChild(svg)
|
||||
ctr.setAttribute(highlightLabelIdAttribute, id)
|
||||
ctr.setAttribute('width', '14px')
|
||||
ctr.setAttribute('height', '14px')
|
||||
|
||||
lastElement.appendChild(ctr)
|
||||
}
|
||||
|
||||
return {
|
||||
prefix,
|
||||
|
|
@ -209,10 +231,10 @@ export function makeHighlightNodeAttributes(
|
|||
|
||||
/**
|
||||
* Given a text selection by user, annotate the article around the selection and
|
||||
* produce a {@link https://github.com/google/diff-match-patch | diff patch}
|
||||
*
|
||||
* produce a {@link https://github.com/google/diff-match-patch | diff patch}
|
||||
*
|
||||
* The diff patch is used for identifying the selection/highlight location
|
||||
*
|
||||
*
|
||||
* @param range text selection range
|
||||
* @returns diff patch
|
||||
*/
|
||||
|
|
@ -304,11 +326,11 @@ const getArticleTextNodes = (
|
|||
|
||||
/**
|
||||
* Return the offsets to the selection/highlight
|
||||
*
|
||||
*
|
||||
* @param patch {@link generateDiffPatch|diff patch} identifying a selection/highlight location
|
||||
* @returns
|
||||
* @returns
|
||||
* - highlightTextStart - The start of highlight, offset from the start of article by characters
|
||||
* - highlightTextEnd - The end of highlight (non-inclusive), offset from the start of article by characters
|
||||
* - highlightTextEnd - The end of highlight (non-inclusive), offset from the start of article by characters
|
||||
* - matchingHighlightContent - the matched highlight
|
||||
*/
|
||||
const selectionOffsetsFromPatch = (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { highlightColor, highlightColorVar } from '../themeUpdater'
|
||||
import { highlightColorVar } from '../themeUpdater'
|
||||
|
||||
export type SelectionAttributes = {
|
||||
selection: Selection
|
||||
|
|
@ -14,6 +14,7 @@ export type SelectionAttributes = {
|
|||
|
||||
export const highlightIdAttribute = 'omnivore-highlight-id'
|
||||
export const highlightNoteIdAttribute = 'omnivore-highlight-note-id'
|
||||
export const highlightLabelIdAttribute = 'omnivore-highlight-label-id'
|
||||
|
||||
export function getHighlightElements(highlightId: string): Element[] {
|
||||
return Array.from(
|
||||
|
|
@ -46,3 +47,23 @@ export function noteImage(color: string | undefined): SVGSVGElement {
|
|||
svg.appendChild(path)
|
||||
return svg
|
||||
}
|
||||
|
||||
export const labelsImage = (color: string | undefined): SVGSVGElement => {
|
||||
const svgURI = 'http://www.w3.org/2000/svg'
|
||||
const svg = document.createElementNS(svgURI, 'svg')
|
||||
svg.setAttribute('viewBox', '0 0 14 14')
|
||||
svg.setAttribute('width', '14')
|
||||
svg.setAttribute('height', '14')
|
||||
svg.setAttribute('fill', 'none')
|
||||
|
||||
const path = document.createElementNS(svgURI, 'path')
|
||||
path.setAttribute(
|
||||
'd',
|
||||
'M1.75 3.5V6.517C1.75007 6.82639 1.87303 7.12309 2.09183 7.34183L6.58933 11.8393C6.85297 12.1029 7.21052 12.251 7.58333 12.251C7.95615 12.251 8.3137 12.1029 8.57733 11.8393L11.8393 8.57733C12.1029 8.3137 12.251 7.95615 12.251 7.58333C12.251 7.21052 12.1029 6.85297 11.8393 6.58933L7.34183 2.09183C7.12309 1.87303 6.82639 1.75007 6.517 1.75H3.5C3.03587 1.75 2.59075 1.93437 2.26256 2.26256C1.93437 2.59075 1.75 3.03587 1.75 3.5Z'
|
||||
)
|
||||
path.setAttribute('stroke', `rgba(${highlightColorVar(color)}, 0.8)`)
|
||||
path.setAttribute('stroke-width', '1.8')
|
||||
path.setAttribute('stroke-linejoin', 'round')
|
||||
svg.appendChild(path)
|
||||
return svg
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export type Subscription = {
|
|||
newsletterEmail?: string
|
||||
|
||||
url?: string
|
||||
icon?: string
|
||||
description?: string
|
||||
|
||||
status: SubscriptionStatus
|
||||
|
|
@ -57,6 +58,7 @@ export function useGetSubscriptionsQuery(
|
|||
type
|
||||
newsletterEmail
|
||||
url
|
||||
icon
|
||||
description
|
||||
status
|
||||
unsubscribeMailTo
|
||||
|
|
|
|||
582
packages/web/pages/settings/shortcuts.tsx
Normal file
582
packages/web/pages/settings/shortcuts.tsx
Normal file
|
|
@ -0,0 +1,582 @@
|
|||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
|
||||
import { SettingsLayout } from '../../components/templates/SettingsLayout'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
SpanBox,
|
||||
Separator,
|
||||
} from '../../components/elements/LayoutPrimitives'
|
||||
import { LabelChip } from '../../components/elements/LabelChip'
|
||||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionType,
|
||||
useGetSubscriptionsQuery,
|
||||
} from '../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { DragIcon } from '../../components/elements/icons/DragIcon'
|
||||
import { CoverImage } from '../../components/elements/CoverImage'
|
||||
import { Label } from '../../lib/networking/fragments/labelFragment'
|
||||
import { usePersistedState } from '../../lib/hooks/usePersistedState'
|
||||
import { CheckSquare, Square } from 'phosphor-react'
|
||||
import { Button } from '../../components/elements/Button'
|
||||
import { styled } from '@stitches/react'
|
||||
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
|
||||
|
||||
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
|
||||
|
||||
const SHORTCUTS_KEY = 'library-shortcuts'
|
||||
|
||||
export default function Shortcuts(): JSX.Element {
|
||||
applyStoredTheme()
|
||||
const [navMenuStyle, setNavMenuStyle] = usePersistedState<
|
||||
'legacy' | 'shortcuts'
|
||||
>({
|
||||
key: 'library-nav-menu-style',
|
||||
initialValue: 'legacy',
|
||||
})
|
||||
|
||||
const listReducer = (
|
||||
state: { state: string; items: Shortcut[] },
|
||||
action: {
|
||||
type: ListAction
|
||||
item?: Shortcut
|
||||
}
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case 'RESET': {
|
||||
const itemStr = window['localStorage'].getItem(SHORTCUTS_KEY)
|
||||
if (itemStr) {
|
||||
try {
|
||||
const parsed = JSON.parse(itemStr)
|
||||
if (Array.isArray(parsed)) {
|
||||
return { state: 'CURRENT', items: parsed as Shortcut[] }
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error: ', err)
|
||||
}
|
||||
}
|
||||
return { state: 'CURRENT', items: [] }
|
||||
}
|
||||
case 'ADD_ITEM': {
|
||||
const item = action.item
|
||||
if (!item) {
|
||||
return state
|
||||
}
|
||||
const existing = state.items.find(
|
||||
(existing) => existing.type == item.type && existing.id == item.id
|
||||
)
|
||||
if (existing) {
|
||||
return state
|
||||
}
|
||||
state.items.push(item)
|
||||
return { state: 'CURRENT', items: [...state.items] }
|
||||
}
|
||||
case 'REMOVE_ITEM': {
|
||||
const item = action.item
|
||||
if (!item) {
|
||||
return state
|
||||
}
|
||||
const updated = state.items.filter((existing) => existing.id != item.id)
|
||||
return { state: 'CURRENT', items: [...updated] }
|
||||
}
|
||||
default:
|
||||
throw new Error('unknown action')
|
||||
}
|
||||
}
|
||||
|
||||
const [shortcuts, dispatchList] = useReducer(listReducer, {
|
||||
state: 'INITIAL',
|
||||
items: [],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (shortcuts.state == 'CURRENT') {
|
||||
window['localStorage'].setItem(
|
||||
SHORTCUTS_KEY,
|
||||
JSON.stringify(shortcuts.items)
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error": ', error)
|
||||
}
|
||||
}, [shortcuts])
|
||||
|
||||
useEffect(() => {
|
||||
dispatchList({ type: 'RESET' })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
<VStack
|
||||
css={{ width: '100%', height: '100%' }}
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
padding: '24px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
gap: '25px',
|
||||
minWidth: '300px',
|
||||
maxWidth: '880px',
|
||||
}}
|
||||
>
|
||||
<Box css={{}}>
|
||||
<StyledText style="fixedHeadline" css={{ my: '6px' }}>
|
||||
Shortcuts
|
||||
</StyledText>
|
||||
<StyledText style="caption" css={{}}>
|
||||
Use shortcuts to access your most important reads quickly
|
||||
</StyledText>
|
||||
</Box>
|
||||
<HStack
|
||||
css={{
|
||||
px: '10px',
|
||||
pt: '2px',
|
||||
height: '30px',
|
||||
gap: '5px',
|
||||
|
||||
fontSize: '14px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color:
|
||||
navMenuStyle !== 'shortcuts'
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
onClick={(event) => {
|
||||
setNavMenuStyle(
|
||||
navMenuStyle == 'shortcuts' ? 'legacy' : 'shortcuts'
|
||||
)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{navMenuStyle === 'shortcuts' ? (
|
||||
<CheckSquare size={20} weight="duotone" />
|
||||
) : (
|
||||
<Square size={20} weight="duotone" />
|
||||
)}
|
||||
<StyledText style="modalTitle" css={{}}>
|
||||
Enable shortcuts
|
||||
</StyledText>
|
||||
</HStack>
|
||||
<Box
|
||||
css={{
|
||||
py: '$3',
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridAutoRows: 'auto',
|
||||
borderRadius: '6px',
|
||||
gridGap: '40px',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
'@mdDown': {
|
||||
gridTemplateColumns: 'repeat(1, 1fr)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SelectedItems
|
||||
shortcuts={shortcuts.items}
|
||||
dispatchList={dispatchList}
|
||||
/>
|
||||
<AvailableItems
|
||||
shortcuts={shortcuts.items}
|
||||
dispatchList={dispatchList}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export const SectionSeparator = styled(Separator, {
|
||||
height: '1px',
|
||||
my: '30px',
|
||||
backgroundColor: '$grayBorder',
|
||||
})
|
||||
|
||||
type ListProps = {
|
||||
shortcuts: Shortcut[]
|
||||
dispatchList: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
|
||||
}
|
||||
|
||||
const AvailableItems = (props: ListProps): JSX.Element => {
|
||||
const { labels } = useGetLabelsQuery()
|
||||
const { savedSearches } = useGetSavedSearchQuery()
|
||||
const { subscriptions } = useGetSubscriptionsQuery()
|
||||
|
||||
const sortedLabels = useMemo(() => {
|
||||
if (!labels) {
|
||||
return []
|
||||
}
|
||||
return labels.sort((a, b) =>
|
||||
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
|
||||
)
|
||||
}, [labels])
|
||||
|
||||
const sortedSubscriptions = useMemo(() => {
|
||||
if (!subscriptions) {
|
||||
return []
|
||||
}
|
||||
return subscriptions.sort((a, b) =>
|
||||
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
|
||||
)
|
||||
}, [subscriptions])
|
||||
|
||||
const sortedsavedSearches = useMemo(() => {
|
||||
if (!savedSearches) {
|
||||
return []
|
||||
}
|
||||
return savedSearches.sort((a, b) =>
|
||||
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
|
||||
)
|
||||
}, [savedSearches])
|
||||
|
||||
const searchSelected = useCallback(
|
||||
(search: SavedSearch) => {
|
||||
return !!props.shortcuts.find((shortcut) => shortcut.id == search.id)
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
||||
const labelSelected = useCallback(
|
||||
(label: Label) => {
|
||||
return !!props.shortcuts.find((shortcut) => shortcut.id == label.id)
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
||||
const subscriptionSelected = useCallback(
|
||||
(subscription: Subscription) => {
|
||||
return !!props.shortcuts.find(
|
||||
(shortcut) => shortcut.id == subscription.id
|
||||
)
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
width: '420px',
|
||||
py: '30px',
|
||||
pl: '28px', // becomes labels have some margin built in
|
||||
pr: '30px',
|
||||
gap: '10px',
|
||||
bg: '$thLeftMenuBackground',
|
||||
}}
|
||||
>
|
||||
<StyledText style="settingsSection">Saved Searches</StyledText>
|
||||
{sortedsavedSearches?.map((search) => {
|
||||
return (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
p: '5px',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
'&:hover': {
|
||||
bg: '$thLibrarySelectionColor',
|
||||
color: '$thLibraryMenuSecondary',
|
||||
},
|
||||
}}
|
||||
key={`search-${search.id}`}
|
||||
onClick={(event) => {
|
||||
const item: Shortcut = {
|
||||
id: search.id,
|
||||
name: search.name,
|
||||
type: 'label',
|
||||
filter: search.filter,
|
||||
}
|
||||
props.dispatchList({
|
||||
item,
|
||||
type: searchSelected(search) ? 'REMOVE_ITEM' : 'ADD_ITEM',
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<StyledText style="settingsItem">{search.name}</StyledText>
|
||||
<SpanBox css={{ ml: 'auto' }}>
|
||||
{searchSelected(search) ? (
|
||||
<CheckSquare size={20} weight="duotone" />
|
||||
) : (
|
||||
<Square size={20} weight="duotone" />
|
||||
)}
|
||||
</SpanBox>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
<SectionSeparator />
|
||||
|
||||
<StyledText style="settingsSection">Labels</StyledText>
|
||||
{sortedLabels.map((label) => {
|
||||
return (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
p: '5px',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
'&:hover': {
|
||||
bg: '$thLibrarySelectionColor',
|
||||
color: '$thLibraryMenuSecondary',
|
||||
},
|
||||
}}
|
||||
key={`label-${label.id}`}
|
||||
onClick={(event) => {
|
||||
const item: Shortcut = {
|
||||
id: label.id,
|
||||
type: 'label',
|
||||
label: label,
|
||||
name: label.name,
|
||||
filter: `label:\"${label.name}\"`,
|
||||
}
|
||||
props.dispatchList({
|
||||
item,
|
||||
type: labelSelected(label) ? 'REMOVE_ITEM' : 'ADD_ITEM',
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<LabelChip text={label.name} color={label.color} />
|
||||
<SpanBox css={{ ml: 'auto' }}>
|
||||
{labelSelected(label) ? (
|
||||
<CheckSquare size={20} weight="duotone" />
|
||||
) : (
|
||||
<Square size={20} weight="duotone" />
|
||||
)}
|
||||
</SpanBox>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
<SectionSeparator />
|
||||
|
||||
<StyledText style="settingsSection">Subscriptions</StyledText>
|
||||
{sortedSubscriptions.map((subscription) => {
|
||||
return (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
p: '5px',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
'&:hover': {
|
||||
bg: '$thLibrarySelectionColor',
|
||||
color: '$thLibraryMenuSecondary',
|
||||
},
|
||||
}}
|
||||
key={`subscription-${subscription.id}`}
|
||||
onClick={(event) => {
|
||||
const item: Shortcut = {
|
||||
id: subscription.id,
|
||||
name: subscription.name,
|
||||
icon: subscription.icon,
|
||||
type:
|
||||
subscription.type == SubscriptionType.NEWSLETTER
|
||||
? 'newsletter'
|
||||
: 'feed',
|
||||
filter:
|
||||
subscription.type == SubscriptionType.NEWSLETTER
|
||||
? `subscription:\"${subscription.name}\"`
|
||||
: `rss:\"${subscription.url}\"`,
|
||||
}
|
||||
props.dispatchList({
|
||||
item,
|
||||
type: subscriptionSelected(subscription)
|
||||
? 'REMOVE_ITEM'
|
||||
: 'ADD_ITEM',
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<StyledText style="settingsItem">{subscription.name}</StyledText>
|
||||
<SpanBox css={{ ml: 'auto' }}>
|
||||
{subscriptionSelected(subscription) ? (
|
||||
<CheckSquare size={20} weight="duotone" />
|
||||
) : (
|
||||
<Square size={20} weight="duotone" />
|
||||
)}
|
||||
</SpanBox>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type AvailableItemButtonProps = {
|
||||
shortcut: Shortcut
|
||||
isSelected: boolean
|
||||
listAction: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
|
||||
}
|
||||
|
||||
const AvailableItemButton = (props: AvailableItemButtonProps): JSX.Element => {
|
||||
const shortcutId = `checkbox-search-${props.shortcut.id}`
|
||||
return (
|
||||
<CheckboxButton
|
||||
itemKey={shortcutId}
|
||||
title={props.shortcut.filter}
|
||||
isSelected={props.isSelected}
|
||||
item={props.shortcut}
|
||||
listAction={props.listAction}
|
||||
>
|
||||
<SpanBox css={{}}>{props.shortcut.name}</SpanBox>
|
||||
</CheckboxButton>
|
||||
)
|
||||
}
|
||||
|
||||
export type Shortcut = {
|
||||
type: 'search' | 'label' | 'newsletter' | 'feed'
|
||||
|
||||
id: string
|
||||
name: string
|
||||
filter: string
|
||||
|
||||
icon?: string
|
||||
label?: Label
|
||||
}
|
||||
|
||||
const SelectedItems = (props: ListProps): JSX.Element => {
|
||||
return (
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '420px',
|
||||
py: '30px',
|
||||
pl: '28px', // becomes labels have some margin built in
|
||||
pr: '30px',
|
||||
gap: '15px',
|
||||
bg: '$thLeftMenuBackground',
|
||||
}}
|
||||
>
|
||||
<StyledText style="settingsSection">Your shortcuts</StyledText>
|
||||
{props.shortcuts.map((shortcut) => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ width: '100%', gap: '10px' }}
|
||||
key={`search-${shortcut.id}`}
|
||||
>
|
||||
<CoverImage src={shortcut.icon} width={20} height={20} />
|
||||
<StyledText style="settingsItem">{shortcut.name}</StyledText>
|
||||
<HStack css={{ ml: 'auto' }}>
|
||||
<DragIcon />
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
})}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type CheckboxButtonProps = {
|
||||
itemKey: string
|
||||
title: string
|
||||
isSelected: boolean
|
||||
item: Shortcut
|
||||
|
||||
listAction: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function CheckboxButton(props: CheckboxButtonProps): JSX.Element {
|
||||
const handleChange = useCallback(
|
||||
(selected: boolean) => {
|
||||
if (!selected) {
|
||||
props.listAction({
|
||||
type: 'REMOVE_ITEM',
|
||||
item: props.item,
|
||||
})
|
||||
} else {
|
||||
props.listAction({
|
||||
type: 'ADD_ITEM',
|
||||
item: props.item,
|
||||
})
|
||||
}
|
||||
},
|
||||
[props]
|
||||
)
|
||||
return (
|
||||
<HStack
|
||||
key={props.itemKey}
|
||||
title={props.title}
|
||||
css={{
|
||||
px: '10px',
|
||||
pt: '2px',
|
||||
height: '30px',
|
||||
gap: '5px',
|
||||
|
||||
fontSize: '14px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: props.isSelected
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
|
||||
m: '0px',
|
||||
'&:hover': {
|
||||
backgroundColor: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
onClick={(event) => {
|
||||
handleChange(!props.isSelected)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.isSelected ? (
|
||||
<CheckSquare size={20} weight="duotone" />
|
||||
) : (
|
||||
<Square size={20} weight="duotone" />
|
||||
)}
|
||||
{props.children}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -44,23 +44,25 @@
|
|||
font-family: var(--text-font-family);
|
||||
}
|
||||
|
||||
.article-inner-css .highlight_with_note .highlight_note_button {
|
||||
.article-inner-css .highlight_note_button {
|
||||
display: unset !important;
|
||||
margin: 0px !important;
|
||||
max-width: unset !important;
|
||||
height: unset !important;
|
||||
padding: 0px 8px;
|
||||
cursor: pointer;
|
||||
margin-left: 5px !important;
|
||||
}
|
||||
|
||||
.article-inner-css .highlight_label_button {
|
||||
display: unset !important;
|
||||
margin: 0px !important;
|
||||
max-width: unset !important;
|
||||
height: unset !important;
|
||||
padding: 0px 8px;
|
||||
cursor: pointer;
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
/*
|
||||
on smaller screens we display the note icon
|
||||
@media (max-width: 768px) {
|
||||
.highlight_with_note.last_element:after {
|
||||
content: url(/static/icons/highlight-note-icon.svg);
|
||||
padding: 0 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
} */
|
||||
|
||||
.article-inner-css h1,
|
||||
.article-inner-css h2,
|
||||
|
|
|
|||
Loading…
Reference in a new issue