mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1601 from omnivore-app/feat/library-cards
feat/library cards
This commit is contained in:
commit
d3e334d00d
175 changed files with 6601 additions and 5988 deletions
|
|
@ -334,6 +334,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => {
|
|||
'includes',
|
||||
'recommendedBy',
|
||||
'no',
|
||||
'mode',
|
||||
],
|
||||
tokenize: true,
|
||||
})
|
||||
|
|
@ -424,6 +425,9 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => {
|
|||
noFilter && result.noFilters.push(noFilter)
|
||||
break
|
||||
}
|
||||
case 'mode':
|
||||
// mode is ignored and used only by the frontend
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ const App = () => {
|
|||
margin={window.margin}
|
||||
maxWidthPercentage={window.maxWidthPercentage}
|
||||
lineHeight={window.lineHeight}
|
||||
highContrastFont={window.prefersHighContrastFont ?? true}
|
||||
highContrastText={window.prefersHighContrastFont ?? true}
|
||||
articleMutations={{
|
||||
createHighlightMutation: (input) =>
|
||||
mutation('createHighlight', input),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ type AvatarProps = {
|
|||
height: string
|
||||
fallbackText: string
|
||||
tooltip?: string
|
||||
noFade?: boolean
|
||||
}
|
||||
|
||||
export function Avatar(props: AvatarProps): JSX.Element {
|
||||
|
|
@ -19,10 +18,6 @@ export function Avatar(props: AvatarProps): JSX.Element {
|
|||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
<StyledImage
|
||||
src={props.imageURL}
|
||||
css={{ opacity: props.noFade ? 'unset' : '48%' }}
|
||||
/>
|
||||
<StyledFallback>{props.fallbackText}</StyledFallback>
|
||||
</StyledAvatar>
|
||||
)
|
||||
|
|
@ -35,17 +30,6 @@ const StyledAvatar = styled(Root, {
|
|||
verticalAlign: 'middle',
|
||||
overflow: 'hidden',
|
||||
userSelect: 'none',
|
||||
border: '1px solid $grayBorder',
|
||||
})
|
||||
|
||||
const StyledImage = styled(Image, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
|
||||
'&:hover': {
|
||||
opacity: '100%',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledFallback = styled(Fallback, {
|
||||
|
|
@ -54,8 +38,9 @@ const StyledFallback = styled(Fallback, {
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '$2',
|
||||
fontWeight: 700,
|
||||
backgroundColor: '$avatarBg',
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'Inter',
|
||||
color: '$avatarFont',
|
||||
backgroundColor: '$avatarBg',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
import { Avatar } from './../elements/Avatar'
|
||||
import { AngleDownIcon } from './../tokens/icons/AngleDownIcon'
|
||||
import { HStack } from '../elements/LayoutPrimitives'
|
||||
|
||||
type AvatarDropdownProps = {
|
||||
profileImageURL?: string
|
||||
userInitials: string
|
||||
}
|
||||
|
||||
export function AvatarDropdown(props: AvatarDropdownProps): JSX.Element {
|
||||
return (
|
||||
<HStack alignment="center" css={{ gap: '6px' }}>
|
||||
<Avatar
|
||||
imageURL={props.profileImageURL}
|
||||
height='32px'
|
||||
fallbackText={props.userInitials}
|
||||
/>
|
||||
<Avatar height="32px" fallbackText={props.userInitials} />
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,40 @@ export const Button = styled('button', {
|
|||
},
|
||||
},
|
||||
ctaDarkYellow: {
|
||||
border: 0,
|
||||
fontSize: '14px',
|
||||
border: '1px solid transparent',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
fontStyle: 'normal',
|
||||
fontFamily: 'Inter',
|
||||
borderRadius: '8px',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
color: '$omnivoreGray',
|
||||
bg: '$omnivoreCtaYellow',
|
||||
p: '10px 13px',
|
||||
color: '#3D3D3D',
|
||||
bg: '#FFEA9F',
|
||||
p: '10px 15px',
|
||||
'&:hover': {
|
||||
bg: '$omnivoreCtaYellow',
|
||||
},
|
||||
'&:focus': {
|
||||
outline: 'none !important',
|
||||
border: '1px solid $omnivoreCtaYellow',
|
||||
},
|
||||
},
|
||||
cancelGeneric: {
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
fontFamily: 'Inter',
|
||||
cursor: 'pointer',
|
||||
color: '#6A6968',
|
||||
borderRadius: '5px',
|
||||
border: '1px solid transparent',
|
||||
p: '10px 15px',
|
||||
bg: 'transparent',
|
||||
'&:hover': {
|
||||
bg: '#EBEBEB',
|
||||
},
|
||||
'&:focus': {
|
||||
outline: 'none !important',
|
||||
border: '1px solid $omnivoreCtaYellow',
|
||||
},
|
||||
},
|
||||
ctaOutlineYellow: {
|
||||
boxSizing: 'border-box',
|
||||
|
|
@ -65,7 +89,7 @@ export const Button = styled('button', {
|
|||
},
|
||||
'.ctaButtonIcon': {
|
||||
visibility: 'hidden',
|
||||
}
|
||||
},
|
||||
},
|
||||
ctaGray: {
|
||||
border: 0,
|
||||
|
|
@ -211,7 +235,7 @@ export const Button = styled('button', {
|
|||
},
|
||||
'&[data-state="selected"]': {
|
||||
border: '2px solid #F9D354',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -235,6 +259,21 @@ export const IconButton = styled(Button, {
|
|||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
searchButton: {
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: '0px',
|
||||
mr: '5px',
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
color: '#898989',
|
||||
border: 'unset',
|
||||
background: '$thBackground',
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
55
packages/web/components/elements/CloseButton.tsx
Normal file
55
packages/web/components/elements/CloseButton.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { X } from 'phosphor-react'
|
||||
import { useState } from 'react'
|
||||
import { Button } from './Button'
|
||||
import { Box } from './LayoutPrimitives'
|
||||
|
||||
type CloseButtonProps = {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export function CloseButton(props: CloseButtonProps): JSX.Element {
|
||||
const [hover, setHover] = useState(false)
|
||||
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
background: '$thBackground2',
|
||||
'&:hover': {
|
||||
bg: '#898989',
|
||||
},
|
||||
}}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
>
|
||||
<Button
|
||||
css={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: 'auto',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.close()
|
||||
}}
|
||||
>
|
||||
<X
|
||||
width={10}
|
||||
height={10}
|
||||
weight="bold"
|
||||
color={hover ? '#EBEBEB' : '#898989'}
|
||||
className="xMark"
|
||||
/>
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,15 +8,15 @@ import {
|
|||
Arrow,
|
||||
Label,
|
||||
} from '@radix-ui/react-dropdown-menu'
|
||||
import { PopperContentProps } from '@radix-ui/react-popover';
|
||||
import { CSS } from '@stitches/react';
|
||||
import { PopperContentProps } from '@radix-ui/react-popover'
|
||||
import { CSS } from '@stitches/react'
|
||||
import { styled } from './../tokens/stitches.config'
|
||||
|
||||
const itemStyles = {
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
py: '12px',
|
||||
px: '24px',
|
||||
const StyledItem = styled(Item, {
|
||||
fontSize: '14px',
|
||||
fontWeight: '400',
|
||||
py: '10px',
|
||||
px: '15px',
|
||||
borderRadius: 3,
|
||||
cursor: 'default',
|
||||
color: '$utilityTextDefault',
|
||||
|
|
@ -25,9 +25,7 @@ const itemStyles = {
|
|||
outline: 'none',
|
||||
backgroundColor: '$grayBgHover',
|
||||
},
|
||||
}
|
||||
|
||||
const StyledItem = styled(Item, itemStyles)
|
||||
})
|
||||
|
||||
const DropdownTrigger = styled(Trigger, {
|
||||
fontSize: '100%',
|
||||
|
|
@ -44,7 +42,6 @@ const StyledTriggerItem = styled(TriggerItem, {
|
|||
outline: 'none',
|
||||
backgroundColor: '$grayBgHover',
|
||||
},
|
||||
...itemStyles,
|
||||
})
|
||||
|
||||
export const DropdownContent = styled(Content, {
|
||||
|
|
@ -123,6 +120,7 @@ type DropdownProps = {
|
|||
disabled?: boolean
|
||||
css?: CSS
|
||||
modal?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
export const DropdownSeparator = styled(Separator, {
|
||||
|
|
@ -141,14 +139,21 @@ type DropdownOptionProps = {
|
|||
export function DropdownOption(props: DropdownOptionProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<StyledItem onSelect={props.onSelect}>
|
||||
<StyledItem
|
||||
onSelect={() => {
|
||||
props.onSelect()
|
||||
}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{props.title ?? props.children}
|
||||
</StyledItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Dropdown(props: DropdownProps & PopperContentProps): JSX.Element {
|
||||
export function Dropdown(
|
||||
props: DropdownProps & PopperContentProps
|
||||
): JSX.Element {
|
||||
const {
|
||||
children,
|
||||
align,
|
||||
|
|
@ -159,14 +164,15 @@ export function Dropdown(props: DropdownProps & PopperContentProps): JSX.Element
|
|||
sideOffset = 0,
|
||||
alignOffset = 0,
|
||||
css,
|
||||
modal
|
||||
modal,
|
||||
onOpenChange,
|
||||
} = props
|
||||
return (
|
||||
<Root modal={modal}>
|
||||
<Root modal={modal} onOpenChange={props.onOpenChange}>
|
||||
<DropdownTrigger disabled={disabled}>{triggerElement}</DropdownTrigger>
|
||||
<DropdownContent
|
||||
css={css}
|
||||
onInteractOutside={(event) => {
|
||||
onInteractOutside={() => {
|
||||
// remove focus from dropdown
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { EdgeIcon } from './images/EdgeIcon'
|
|||
import { FirefoxIcon } from './images/FirefoxIcon'
|
||||
import { SafariIcon } from './images/SafariIcon'
|
||||
import Link from 'next/link'
|
||||
import { SaveArticleIcon } from './images/SaveArticleIcon'
|
||||
|
||||
const icons = {
|
||||
'Google Chrome': <ChromeIcon />,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
|
||||
import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { Button } from './Button'
|
||||
import { HStack, VStack } from './LayoutPrimitives'
|
||||
import { StyledTextArea } from './StyledTextArea'
|
||||
|
||||
type HighlightNoteTextEditAreaProps = {
|
||||
setIsEditing: (editing: boolean) => void
|
||||
highlight: Highlight
|
||||
updateHighlight: (highlight: Highlight) => void
|
||||
}
|
||||
|
||||
export const HighlightNoteTextEditArea = (
|
||||
props: HighlightNoteTextEditAreaProps
|
||||
): JSX.Element => {
|
||||
const [noteContent, setNoteContent] = useState(
|
||||
props.highlight.annotation ?? ''
|
||||
)
|
||||
|
||||
const handleNoteContentChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
setNoteContent(event.target.value)
|
||||
},
|
||||
[setNoteContent]
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%' }} key="textEditor">
|
||||
<StyledTextArea
|
||||
css={{
|
||||
my: '10px',
|
||||
minHeight: '200px',
|
||||
borderRadius: '5px',
|
||||
p: '10px',
|
||||
width: '100%',
|
||||
marginTop: '16px',
|
||||
resize: 'vertical',
|
||||
bg: '#EBEBEB',
|
||||
color: '#3D3D3D',
|
||||
}}
|
||||
autoFocus
|
||||
maxLength={4000}
|
||||
value={noteContent}
|
||||
placeholder={'Add your notes...'}
|
||||
onChange={handleNoteContentChange}
|
||||
/>
|
||||
<HStack alignment="center" distribution="end" css={{ width: '100%' }}>
|
||||
<Button
|
||||
style="cancelGeneric"
|
||||
css={{ mr: '$2' }}
|
||||
onClick={() => {
|
||||
props.setIsEditing(false)
|
||||
setNoteContent(props.highlight.annotation ?? '')
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
const result = await updateHighlightMutation({
|
||||
highlightId: props.highlight.id,
|
||||
annotation: noteContent,
|
||||
})
|
||||
console.log('result: ' + result)
|
||||
|
||||
if (!result) {
|
||||
showErrorToast('There was an error updating your highlight.')
|
||||
} else {
|
||||
showSuccessToast('Note saved')
|
||||
props.highlight.annotation = noteContent
|
||||
props.updateHighlight(props.highlight)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error updating annoation', err)
|
||||
showErrorToast('There was an error updating your highlight.')
|
||||
}
|
||||
|
||||
props.setIsEditing(false)
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import Link from 'next/link'
|
||||
import { Info } from 'phosphor-react'
|
||||
import { Box, VStack } from '../elements/LayoutPrimitives'
|
||||
import { VStack } from '../elements/LayoutPrimitives'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { TooltipWrapped } from './Tooltip'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getLuminance, lighten, toHsla } from 'color2k'
|
||||
import { getLuminance, lighten } from 'color2k'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Button } from './Button'
|
||||
import { SpanBox } from './LayoutPrimitives'
|
||||
|
|
@ -40,21 +40,16 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
|
|||
<SpanBox
|
||||
css={{
|
||||
display: 'inline-table',
|
||||
margin: '4px',
|
||||
margin: '2px',
|
||||
borderRadius: '4px',
|
||||
color: isDarkMode ? darkThemeTextColor : lightThemeTextColor,
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
padding: '2px 5px 2px 5px',
|
||||
color: lightThemeTextColor,
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
padding: '3px 6px',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer',
|
||||
backgroundClip: 'padding-box',
|
||||
border: isDarkMode
|
||||
? `1px solid ${darkThemeTextColor}`
|
||||
: `1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7)`,
|
||||
backgroundColor: isDarkMode
|
||||
? `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.08)`
|
||||
: props.color,
|
||||
backgroundColor: props.color,
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ const MainContainer = styled(Box, {
|
|||
border: '1px solid $grayBorderHover',
|
||||
},
|
||||
'@mdDown': {
|
||||
width: '100%'
|
||||
}
|
||||
width: '100%',
|
||||
},
|
||||
})
|
||||
|
||||
const CustomLabelWrapper = styled(Box, {
|
||||
|
|
@ -106,8 +106,8 @@ export const LabelColorDropdown = (props: LabelColorDropdownProps) => {
|
|||
} = props
|
||||
|
||||
const isDarkMode = isDarkTheme()
|
||||
const iconColor = isDarkMode ? '#FFFFFF': '#0A0806'
|
||||
const [open, setOpen] = useState<boolean | undefined>(false);
|
||||
const iconColor = isDarkMode ? '#FFFFFF' : '#0A0806'
|
||||
const [open, setOpen] = useState<boolean | undefined>(false)
|
||||
|
||||
const handleCustomColorChange = (color: string) => {
|
||||
setLabelColorHex({
|
||||
|
|
@ -118,7 +118,7 @@ export const LabelColorDropdown = (props: LabelColorDropdownProps) => {
|
|||
|
||||
const handleOpen = (open: boolean) => {
|
||||
if (canEdit && open) setOpen(true)
|
||||
else if((isCreateMode && !canEdit) && open) setOpen(true)
|
||||
else if (isCreateMode && !canEdit && open) setOpen(true)
|
||||
else setOpen(false)
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ export const LabelColorDropdown = (props: LabelColorDropdownProps) => {
|
|||
width: '100%',
|
||||
'@md': {
|
||||
minWidth: '170px',
|
||||
width: 'auto'
|
||||
width: 'auto',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
39
packages/web/components/elements/LogoBox.tsx
Normal file
39
packages/web/components/elements/LogoBox.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { LIBRARY_LEFT_MENU_WIDTH } from '../templates/homeFeed/LibraryFilterMenu'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { OmnivoreFullLogo } from './images/OmnivoreFullLogo'
|
||||
import { OmnivoreNameLogo } from './images/OmnivoreNameLogo'
|
||||
import { SpanBox } from './LayoutPrimitives'
|
||||
|
||||
export function LogoBox(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<SpanBox
|
||||
css={{
|
||||
pl: '25px',
|
||||
height: '24px',
|
||||
width: LIBRARY_LEFT_MENU_WIDTH,
|
||||
minWidth: LIBRARY_LEFT_MENU_WIDTH,
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<OmnivoreFullLogo
|
||||
showTitle={true}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
ml: '15px',
|
||||
mr: '15px',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<OmnivoreNameLogo />
|
||||
</SpanBox>
|
||||
</>
|
||||
)
|
||||
}
|
||||
57
packages/web/components/elements/MenuTriggerButton.tsx
Normal file
57
packages/web/components/elements/MenuTriggerButton.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { DotsThreeVertical, X } from 'phosphor-react'
|
||||
import { useState } from 'react'
|
||||
import { Button } from './Button'
|
||||
import { Box, SpanBox } from './LayoutPrimitives'
|
||||
|
||||
export function MenuTrigger(): JSX.Element {
|
||||
const [hover, setHover] = useState(false)
|
||||
|
||||
return (
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
background: '$thBackground2',
|
||||
'&:hover': {
|
||||
bg: '#898989',
|
||||
},
|
||||
}}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
>
|
||||
<DotsThreeVertical
|
||||
size={25}
|
||||
weight="bold"
|
||||
color={hover ? '#EBEBEB' : '#898989'}
|
||||
/>
|
||||
{/* color="#ADADAD" />
|
||||
<Button
|
||||
css={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: 'auto',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.close()
|
||||
}}
|
||||
>
|
||||
<X
|
||||
width={10}
|
||||
height={10}
|
||||
weight="bold"
|
||||
color={hover ? '#EBEBEB' : '#898989'}
|
||||
className="xMark"
|
||||
/>
|
||||
</Button> */}
|
||||
</SpanBox>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,10 +5,9 @@ import {
|
|||
DeviceMobileCamera,
|
||||
} from 'phosphor-react'
|
||||
import { Box, HStack } from '../elements/LayoutPrimitives'
|
||||
import { StyledText, StyledImg, StyledAnchor } from '../elements/StyledText'
|
||||
import { StyledText, StyledAnchor } from '../elements/StyledText'
|
||||
import { TooltipWrapped } from './Tooltip'
|
||||
import Link from 'next/link'
|
||||
import { InstallationIcon } from './images/InstallationIcon'
|
||||
|
||||
const TooltipStyle = {
|
||||
backgroundColor: '#F9D354',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { Root, Overlay, Content } from '@radix-ui/react-dialog'
|
|||
import { X } from 'phosphor-react'
|
||||
import { styled, keyframes, theme } from '../tokens/stitches.config'
|
||||
import { Button } from './Button'
|
||||
import { HStack } from './LayoutPrimitives'
|
||||
import { CloseButton } from './CloseButton'
|
||||
import { HStack, SpanBox } from './LayoutPrimitives'
|
||||
import { StyledText } from './StyledText'
|
||||
|
||||
export const ModalRoot = styled(Root, {})
|
||||
|
|
@ -17,6 +18,7 @@ export const ModalOverlay = styled(Overlay, {
|
|||
width: '100vw',
|
||||
height: '100vh',
|
||||
position: 'fixed',
|
||||
zIndex: 10,
|
||||
inset: 0,
|
||||
'@media (prefers-reduced-motion: no-preference)': {
|
||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
|
|
@ -29,7 +31,6 @@ const Modal = styled(Content, {
|
|||
boxShadow: theme.shadows.cardBoxShadow.toString(),
|
||||
position: 'fixed',
|
||||
'&:focus': { outline: 'none' },
|
||||
zIndex: '1',
|
||||
})
|
||||
|
||||
export const ModalContent = styled(Modal, {
|
||||
|
|
@ -56,18 +57,12 @@ export const ModalTitleBar = (props: ModalTitleBarProps) => {
|
|||
<HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{ height: '68px', width: '100%' }}
|
||||
css={{ height: '50px', width: '100%' }}
|
||||
>
|
||||
<StyledText style="modalHeadline">{props.title}</StyledText>
|
||||
<Button
|
||||
css={{ ml: 'auto' }}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<X size={24} color={theme.colors.textNonessential.toString()} />
|
||||
</Button>
|
||||
<SpanBox css={{ ml: 'auto' }}>
|
||||
<CloseButton close={() => props.onOpenChange(false)} />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -95,7 +90,7 @@ export const ModalButtonBar = (props: ModalButtonBarProps) => {
|
|||
}}
|
||||
>
|
||||
<Button
|
||||
style={'ctaOutlineYellow'}
|
||||
style={'cancelGeneric'}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
|
|
|
|||
|
|
@ -67,9 +67,10 @@ const textVariants = {
|
|||
margin: 0,
|
||||
},
|
||||
modalHeadline: {
|
||||
fontWeight: '500',
|
||||
fontSize: '24px',
|
||||
lineHeight: '1',
|
||||
fontWeight: '600',
|
||||
fontFamily: '$display',
|
||||
fontSize: '20px',
|
||||
lineHeight: '20px',
|
||||
color: '$grayText',
|
||||
margin: 0,
|
||||
},
|
||||
|
|
@ -178,6 +179,13 @@ const textVariants = {
|
|||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
},
|
||||
displaySettingsLabel: {
|
||||
fontFamily: '$display',
|
||||
fontWeight: '500',
|
||||
fontSize: '12px',
|
||||
lineHeight: '20px',
|
||||
color: '$thTextSubtle2',
|
||||
},
|
||||
error: {
|
||||
color: '$error',
|
||||
fontSize: '$2',
|
||||
|
|
|
|||
|
|
@ -1,39 +1,73 @@
|
|||
|
||||
|
||||
import { Box, HStack } from './LayoutPrimitives'
|
||||
import { styled, theme } from '../tokens/stitches.config'
|
||||
import { Box, HStack, SpanBox } from './LayoutPrimitives'
|
||||
import { styled } from '../tokens/stitches.config'
|
||||
import { Root as Slider, Thumb, Track } from '@radix-ui/react-slider'
|
||||
import { DropdownSeparator } from './DropdownElements'
|
||||
|
||||
type TickedRangeSliderProps = {
|
||||
ticks?: number,
|
||||
value: number,
|
||||
onChange: (value: number) => void,
|
||||
min?: number,
|
||||
max?: number,
|
||||
step?: number,
|
||||
ticks?: number
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
}
|
||||
|
||||
const Tick = styled(Box, {
|
||||
background: theme.colors.grayBorderHover,
|
||||
width: 2,
|
||||
height: 8,
|
||||
const StyledSlider = styled(Slider, {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
'.SliderTrack': {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: '0px',
|
||||
height: '8px',
|
||||
width: '225px',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#F2F2F2',
|
||||
},
|
||||
'.SliderThumb': {
|
||||
display: 'block',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
border: '4px solid white',
|
||||
backgroundColor: '#FFD234',
|
||||
boxShadow: '0px 0px 20px rgba(19, 56, 77, 0.2)',
|
||||
},
|
||||
})
|
||||
|
||||
export function TickedRangeSlider({
|
||||
ticks = 8,
|
||||
min = 10,
|
||||
max = 28,
|
||||
step = 1,
|
||||
value,
|
||||
onChange,
|
||||
} : TickedRangeSliderProps
|
||||
): JSX.Element {
|
||||
|
||||
min = 10,
|
||||
max = 28,
|
||||
step = 1,
|
||||
value,
|
||||
onChange,
|
||||
}: TickedRangeSliderProps): JSX.Element {
|
||||
return (
|
||||
<Box css={{zIndex: 2}}>
|
||||
<input onChange={(e) => onChange(e.target.value as any)} value={value} type="range" min={min} max={max} step={step} className='slider'/>
|
||||
<HStack distribution='between' css={{position: 'relative', bottom: 12.2, left: 2, zIndex: -1}}>
|
||||
{[...Array(ticks)].map((val, idx) => <Tick key={`ticks-${idx}`} />)}
|
||||
</HStack>
|
||||
</Box>
|
||||
<HStack
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '225px',
|
||||
height: '20px',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<StyledSlider
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
value={[value]}
|
||||
onValueChange={(value) => {
|
||||
if (value.length) {
|
||||
onChange(value[0])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Track className="SliderTrack">
|
||||
<Box css={{ bg: '#CECECE', width: '2.5px', height: '100%' }} />
|
||||
</Track>
|
||||
<Thumb className="SliderThumb" />
|
||||
</StyledSlider>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
packages/web/components/elements/images/GridSelectorIcon.tsx
Normal file
38
packages/web/components/elements/images/GridSelectorIcon.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { config } from '../../tokens/stitches.config'
|
||||
|
||||
export type GridSelectorIconProps = {
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function GridSelectorIcon(props: GridSelectorIconProps): JSX.Element {
|
||||
const fillColor = props.color || config.theme.colors.graySolid
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M8.32487 0.170105H1.6582C0.967847 0.170105 0.408203 0.729749 0.408203 1.4201V8.08677C0.408203 8.77713 0.967847 9.33677 1.6582 9.33677H8.32487C9.01523 9.33677 9.57487 8.77713 9.57487 8.08677V1.4201C9.57487 0.729749 9.01523 0.170105 8.32487 0.170105Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M19.1582 0.170105H12.4915C11.8012 0.170105 11.2415 0.729749 11.2415 1.4201V8.08677C11.2415 8.77713 11.8012 9.33677 12.4915 9.33677H19.1582C19.8486 9.33677 20.4082 8.77713 20.4082 8.08677V1.4201C20.4082 0.729749 19.8486 0.170105 19.1582 0.170105Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M8.32487 11.0034H1.6582C0.967847 11.0034 0.408203 11.5631 0.408203 12.2534V18.9201C0.408203 19.6105 0.967847 20.1701 1.6582 20.1701H8.32487C9.01523 20.1701 9.57487 19.6105 9.57487 18.9201V12.2534C9.57487 11.5631 9.01523 11.0034 8.32487 11.0034Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M19.1582 11.0034H12.4915C11.8012 11.0034 11.2415 11.5631 11.2415 12.2534V18.9201C11.2415 19.6105 11.8012 20.1701 12.4915 20.1701H19.1582C19.8486 20.1701 20.4082 19.6105 20.4082 18.9201V12.2534C20.4082 11.5631 19.8486 11.0034 19.1582 11.0034Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
type LineHeightDecreaseIconProps = {
|
||||
strokeColor: string
|
||||
}
|
||||
|
||||
export function LineHeightDecreaseIcon(
|
||||
props: LineHeightDecreaseIconProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="23"
|
||||
height="14"
|
||||
viewBox="0 0 23 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.2728 4.04077L21.8195 4.04077"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.2728 6.92456L21.8195 6.92456"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.2728 9.80811L21.8195 9.80811"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.20044 2.69069L3.27599 1.15112L1.35153 2.69069"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.20044 11.1582L3.27599 12.6978L1.35153 11.1582"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.276 1.15112L3.276 12.6978"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
type LineHeightIncreaseIconProps = {
|
||||
strokeColor: string
|
||||
}
|
||||
|
||||
export function LineHeightIncreaseIcon(
|
||||
props: LineHeightIncreaseIconProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="23"
|
||||
height="14"
|
||||
viewBox="0 0 23 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.2728 2.04089L21.8195 2.04089"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.2728 6.92468L21.8195 6.92468"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.2728 11.8081L21.8195 11.8081"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.20044 2.69069L3.27599 1.15112L1.35153 2.69069"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.20044 11.1583L3.27599 12.6979L1.35153 11.1583"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.276 1.15112L3.276 12.6978"
|
||||
stroke="#969696"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
34
packages/web/components/elements/images/ListSelectorIcon.tsx
Normal file
34
packages/web/components/elements/images/ListSelectorIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { config } from '../../tokens/stitches.config'
|
||||
|
||||
export type ListSelectorIconProps = {
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function ListSelectorIcon(props: ListSelectorIconProps): JSX.Element {
|
||||
const fillColor = props.color || config.theme.colors.graySolid
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M19.1582 0.795105H1.6582C0.967847 0.795105 0.408203 1.35475 0.408203 2.0451V4.12844C0.408203 4.81879 0.967847 5.37844 1.6582 5.37844H19.1582C19.8486 5.37844 20.4082 4.81879 20.4082 4.12844V2.0451C20.4082 1.35475 19.8486 0.795105 19.1582 0.795105Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M19.1582 7.87845H1.6582C0.967847 7.87845 0.408203 8.43809 0.408203 9.12845V11.2118C0.408203 11.9021 0.967847 12.4618 1.6582 12.4618H19.1582C19.8486 12.4618 20.4082 11.9021 20.4082 11.2118V9.12845C20.4082 8.43809 19.8486 7.87845 19.1582 7.87845Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M19.1582 14.9618H1.6582C0.967847 14.9618 0.408203 15.5214 0.408203 16.2118V18.2951C0.408203 18.9855 0.967847 19.5451 1.6582 19.5451H19.1582C19.8486 19.5451 20.4082 18.9855 20.4082 18.2951V16.2118C20.4082 15.5214 19.8486 14.9618 19.1582 14.9618Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
72
packages/web/components/elements/images/OmnivoreFullLogo.tsx
Normal file
72
packages/web/components/elements/images/OmnivoreFullLogo.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import Link from 'next/link'
|
||||
import { config } from '../../tokens/stitches.config'
|
||||
|
||||
export type OmnivoreFullLogoProps = {
|
||||
color?: string
|
||||
href?: string
|
||||
showTitle?: boolean
|
||||
}
|
||||
|
||||
export function OmnivoreFullLogo(props: OmnivoreFullLogoProps): JSX.Element {
|
||||
const fillColor = props.color || config.theme.colors.graySolid
|
||||
const href = props.href || '/home'
|
||||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<a
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="129"
|
||||
height="26"
|
||||
viewBox="0 0 129 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M43.4131 13.2862C43.4131 9.75626 41.2214 7.74365 38.3606 7.74365C35.4839 7.74365 33.308 9.75626 33.308 13.2862C33.308 16.8004 35.4839 18.8288 38.3606 18.8288C41.2214 18.8288 43.4131 16.8162 43.4131 13.2862ZM41.1002 13.2862C41.1002 15.5728 40.0149 16.8109 38.3606 16.8109C36.701 16.8109 35.6209 15.5728 35.6209 13.2862C35.6209 10.9996 36.701 9.76153 38.3606 9.76153C40.0149 9.76153 41.1002 10.9996 41.1002 13.2862Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M47.3285 7.89117V18.6813H49.5413V11.6319H49.6309L52.4232 18.6286H53.9301L56.7224 11.6582H56.812V18.6813H59.0248V7.89117H56.2114L53.2399 15.1408H53.1134L50.1419 7.89117H47.3285Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M72.1549 7.89117H69.8842V14.6771H69.7893L65.1319 7.89117H63.1298V18.6813H65.4111V11.89H65.4901L70.1845 18.6813H72.1549V7.89117Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M78.5465 7.89117H76.2652V18.6813H78.5465V7.89117Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M84.5983 7.89117H82.0641L85.789 18.6813H88.7289L92.4485 7.89117H89.9196L87.3063 16.0891H87.2062L84.5983 7.89117Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M105.28 13.2862C105.28 9.75626 103.088 7.74365 100.227 7.74365C97.3504 7.74365 95.1745 9.75626 95.1745 13.2862C95.1745 16.8004 97.3504 18.8288 100.227 18.8288C103.088 18.8288 105.28 16.8162 105.28 13.2862ZM102.967 13.2862C102.967 15.5728 101.881 16.8109 100.227 16.8109C98.5675 16.8109 97.4874 15.5728 97.4874 13.2862C97.4874 10.9996 98.5675 9.76153 100.227 9.76153C101.881 9.76153 102.967 10.9996 102.967 13.2862Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M109.195 18.6813H111.476V14.8563H113.141L115.185 18.6813H117.704L115.412 14.4875C116.64 13.9606 117.319 12.8911 117.319 11.4159C117.319 9.27155 115.902 7.89117 113.452 7.89117H109.195V18.6813ZM111.476 13.0228V9.75626H113.015C114.332 9.75626 114.969 10.3411 114.969 11.4159C114.969 12.4854 114.332 13.0228 113.025 13.0228H111.476Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M121.157 18.6813H128.449V16.8004H123.438V14.224H128.053V12.3431H123.438V9.77206H128.427V7.89117H121.157V18.6813Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M8.42285 17.9061V10.5447C8.42285 9.91527 9.16173 9.55951 9.65432 9.99737L11.9257 13.3087C12.3909 13.6918 13.0477 13.6918 13.5129 13.3087L15.7296 10.0247C16.2222 9.61424 16.961 9.94263 16.961 10.5721V14.458C16.961 16.3463 18.2199 17.8788 20.1081 17.8788H20.1629C21.7775 17.8788 23.1731 16.7841 23.5563 15.2243C23.7478 14.4033 23.912 13.5549 23.912 12.8982C23.8847 6.46715 18.4388 1.596 11.9257 2.03385C6.39776 2.41698 1.9371 6.87764 1.55397 12.4056C1.11612 18.9187 6.26093 24.3645 12.7193 24.3645"
|
||||
stroke={fillColor}
|
||||
strokeWidth="2.18182"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -20,6 +20,25 @@ type OmnivoreLogoProps = {
|
|||
strokeColor: string
|
||||
}
|
||||
|
||||
export function OmnivoreSmallLogo(props: OmnivoreLogoProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="19"
|
||||
viewBox="0 0 18 19"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.82329 12.9213V7.47867C5.82329 7.01332 6.36957 6.75029 6.73377 7.07402L8.4131 9.5222C8.75706 9.80546 9.24265 9.80546 9.58661 9.5222L11.2255 7.09425C11.5897 6.79075 12.136 7.03355 12.136 7.49891V10.372C12.136 11.768 13.0667 12.9011 14.4627 12.9011H14.5032C15.6969 12.9011 16.7288 12.0918 17.0121 10.9385C17.1537 10.3315 17.2751 9.70429 17.2751 9.2187C17.2549 4.46397 13.2285 0.862512 8.4131 1.18624C4.32605 1.4695 1.02808 4.76746 0.744823 8.85451C0.421097 13.6699 4.22489 17.6963 8.99985 17.6963"
|
||||
stroke={props.strokeColor}
|
||||
strokeWidth="1.4"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function OmnivoreLogoIcon(props: OmnivoreLogoProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
|
|
@ -42,6 +61,7 @@ export function OmnivoreLogoIcon(props: OmnivoreLogoProps): JSX.Element {
|
|||
export type OmnivoreNameLogoProps = {
|
||||
color?: string
|
||||
href?: string
|
||||
showTitle?: boolean
|
||||
}
|
||||
|
||||
export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element {
|
||||
|
|
@ -50,9 +70,22 @@ export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element {
|
|||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<a style={{ textDecoration: 'none', display: 'flex', alignItems: 'center' }}>
|
||||
<a
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<OmnivoreLogoIcon size={27} strokeColor={fillColor}></OmnivoreLogoIcon>
|
||||
{/* <StyledText style="logoTitle" css={{ color: fillColor, paddingLeft: '12px' }}>Omnivore</StyledText> */}
|
||||
{props.showTitle && (
|
||||
<StyledText
|
||||
style="logoTitle"
|
||||
css={{ color: fillColor, paddingLeft: '12px' }}
|
||||
>
|
||||
Omnivore
|
||||
</StyledText>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,4 @@
|
|||
import { config } from '../../tokens/stitches.config'
|
||||
|
||||
type PenWithColorProps = {
|
||||
color?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export function PenWithColorIcon({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
}: PenWithColorProps): JSX.Element {
|
||||
export function PenWithColorIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
|
|
|
|||
20
packages/web/components/elements/images/SideBarIcon.tsx
Normal file
20
packages/web/components/elements/images/SideBarIcon.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
type SideBarProps = {
|
||||
strokeColor: string
|
||||
}
|
||||
|
||||
export function SideBarIcon(props: SideBarProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="19"
|
||||
height="14"
|
||||
viewBox="0 0 19 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.10293 12.2383H7.3014V1.08129H6.10293V12.2383ZM4.62964 3.9129C4.74669 3.9129 4.84847 3.87289 4.93499 3.79286C5.0215 3.70812 5.06476 3.61633 5.06476 3.51747C5.06476 3.40919 5.0215 3.31739 4.93499 3.24207C4.84847 3.16204 4.74669 3.12203 4.62964 3.12203H2.99606C2.87901 3.12203 2.77723 3.16204 2.69071 3.24207C2.6042 3.31739 2.56094 3.40919 2.56094 3.51747C2.56094 3.61633 2.6042 3.70812 2.69071 3.79286C2.77723 3.87289 2.87901 3.9129 2.99606 3.9129H4.62964ZM4.62964 5.7418C4.74669 5.7418 4.84847 5.70179 4.93499 5.62176C5.0215 5.54173 5.06476 5.44758 5.06476 5.3393C5.06476 5.23574 5.0215 5.14629 4.93499 5.07097C4.84847 4.99094 4.74669 4.95093 4.62964 4.95093H2.99606C2.87901 4.95093 2.77723 4.99094 2.69071 5.07097C2.6042 5.14629 2.56094 5.23574 2.56094 5.3393C2.56094 5.44758 2.6042 5.54173 2.69071 5.62176C2.77723 5.70179 2.87901 5.7418 2.99606 5.7418H4.62964ZM4.62964 7.56364C4.74669 7.56364 4.84847 7.52598 4.93499 7.45066C5.0215 7.37063 5.06476 7.27883 5.06476 7.17526C5.06476 7.06699 5.0215 6.97519 4.93499 6.89987C4.84847 6.81984 4.74669 6.77982 4.62964 6.77982H2.99606C2.87901 6.77982 2.77723 6.81984 2.69071 6.89987C2.6042 6.97519 2.56094 7.06699 2.56094 7.17526C2.56094 7.27883 2.6042 7.37063 2.69071 7.45066C2.77723 7.52598 2.87901 7.56364 2.99606 7.56364H4.62964ZM2.68308 13.1562H15.8892C16.6933 13.1562 17.2938 12.9727 17.6907 12.6055C18.0877 12.243 18.2861 11.6969 18.2861 10.9672V2.34528C18.2861 1.6156 18.0877 1.06952 17.6907 0.707038C17.2938 0.339846 16.6933 0.15625 15.8892 0.15625H2.68308C1.8841 0.15625 1.28359 0.339846 0.881553 0.707038C0.484606 1.06952 0.286133 1.6156 0.286133 2.34528V10.9672C0.286133 11.6969 0.484606 12.243 0.881553 12.6055C1.28359 12.9727 1.8841 13.1562 2.68308 13.1562ZM2.69835 12.0194C2.31667 12.0194 2.02405 11.9276 1.82048 11.744C1.61692 11.5557 1.51514 11.2779 1.51514 10.9107V2.40177C1.51514 2.03458 1.61692 1.75918 1.82048 1.57559C2.02405 1.38728 2.31667 1.29313 2.69835 1.29313H15.8739C16.2505 1.29313 16.5406 1.38728 16.7441 1.57559C16.9528 1.75918 17.0571 2.03458 17.0571 2.40177V10.9107C17.0571 11.2779 16.9528 11.5557 16.7441 11.744C16.5406 11.9276 16.2505 12.0194 15.8739 12.0194H2.69835Z"
|
||||
fill="#898989"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { Root, Content, Anchor, Arrow } from '@radix-ui/react-popover'
|
||||
import type { ReactNode } from 'react'
|
||||
import { styled } from '../tokens/stitches.config'
|
||||
|
||||
type AnchoredPopoverProps = {
|
||||
children: ReactNode
|
||||
xAnchorCoordinate: number
|
||||
yAnchorCoordinate: number
|
||||
preventAutoFocus: boolean
|
||||
}
|
||||
|
||||
export function AnchoredPopover(props: AnchoredPopoverProps): JSX.Element {
|
||||
const StyledAnchor = styled(Anchor, {
|
||||
position: 'absolute',
|
||||
left: props.xAnchorCoordinate,
|
||||
top: props.yAnchorCoordinate,
|
||||
})
|
||||
|
||||
const StyledArrow = styled(Arrow, {
|
||||
fill: '$grayBase',
|
||||
})
|
||||
|
||||
return (
|
||||
<Root defaultOpen modal>
|
||||
<StyledAnchor />
|
||||
<Content
|
||||
onOpenAutoFocus={(event) => {
|
||||
if (props.preventAutoFocus) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
<StyledArrow />
|
||||
</Content>
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,14 +5,13 @@ import { formattedLongDate } from './../../lib/dateFormatting'
|
|||
type SubtitleStyle = 'footnote' | 'shareSubtitle'
|
||||
|
||||
type ArticleSubtitleProps = {
|
||||
rawDisplayDate: string
|
||||
href: string
|
||||
author?: string
|
||||
style?: SubtitleStyle
|
||||
hideButton?: boolean
|
||||
}
|
||||
|
||||
export function ArticleSubtitle(props: ArticleSubtitleProps): JSX.Element {
|
||||
export function ArticleSubtitle(props: ArticleSubtitleProps): JSX.Element {
|
||||
const textStyle = props.style || 'footnote'
|
||||
const subtitle = articleSubtitle(props.href, props.author)
|
||||
|
||||
|
|
@ -20,11 +19,11 @@ export function ArticleSubtitle(props: ArticleSubtitleProps): JSX.Element {
|
|||
<Box>
|
||||
<StyledText style={textStyle} css={{ wordBreak: 'break-word' }}>
|
||||
{subtitle}{' '}
|
||||
{subtitle && (<span style={{ position: 'relative', bottom: 1 }}>• </span>)}{' '}
|
||||
{formattedLongDate(props.rawDisplayDate)}{' '}
|
||||
{subtitle && (
|
||||
<span style={{ position: 'relative', bottom: 1 }}>• </span>
|
||||
)}{' '}
|
||||
{!props.hideButton && !shouldHideUrl(props.href) && (
|
||||
<>
|
||||
<span style={{ position: 'relative', bottom: 1 }}>• </span>{' '}
|
||||
<StyledLink
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
|
|
@ -43,6 +42,20 @@ export function ArticleSubtitle(props: ArticleSubtitleProps): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
type ReaderSavedInfoProps = {
|
||||
rawDisplayDate: string
|
||||
}
|
||||
|
||||
export function ReaderSavedInfo(props: ReaderSavedInfoProps): JSX.Element {
|
||||
return (
|
||||
<Box>
|
||||
<StyledText css={{ wordBreak: 'break-word', fontSize: '15' }}>
|
||||
{formattedLongDate(props.rawDisplayDate)}{' '}
|
||||
</StyledText>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function shouldHideUrl(url: string): boolean {
|
||||
const origin = new URL(url).origin
|
||||
const hideHosts = ['https://storage.googleapis.com', 'https://omnivore.app']
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ export type CardMenuDropdownAction =
|
|||
| 'archive'
|
||||
| 'unarchive'
|
||||
| 'delete'
|
||||
| 'share'
|
||||
| 'snooze'
|
||||
| 'set-labels'
|
||||
| 'showOriginal'
|
||||
| 'unsubscribe'
|
||||
|
|
@ -22,11 +20,15 @@ type CardMenuProps = {
|
|||
viewer: UserBasicData
|
||||
triggerElement: ReactNode
|
||||
actionHandler: (action: CardMenuDropdownAction) => void
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function CardMenu(props: CardMenuProps): JSX.Element {
|
||||
return (
|
||||
<Dropdown triggerElement={props.triggerElement}>
|
||||
<Dropdown
|
||||
triggerElement={props.triggerElement}
|
||||
onOpenChange={props.onOpenChange}
|
||||
>
|
||||
{!props.item.isArchived ? (
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('archive')}
|
||||
|
|
@ -52,20 +54,6 @@ export function CardMenu(props: CardMenuProps): JSX.Element {
|
|||
onSelect={() => props.actionHandler('editTitle')}
|
||||
title="Edit Metadata"
|
||||
/>
|
||||
{isVipUser(props.viewer) && (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
props.actionHandler('snooze')
|
||||
}}
|
||||
title="Snooze"
|
||||
/>
|
||||
)}
|
||||
{/* <DropdownOption
|
||||
onSelect={() => {
|
||||
props.actionHandler('share')
|
||||
}}
|
||||
title="Share"
|
||||
/> */}
|
||||
{props.item.readingProgressPercent < 98 ? (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import {
|
|||
import { VStack, HStack } from '../elements/LayoutPrimitives'
|
||||
import { Button } from '../elements/Button'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { useConfirmListener } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
type ConfirmationModalProps = {
|
||||
message?: string
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { isAndroid } from '../../lib/deviceType'
|
||||
|
||||
import { styled, theme } from '../tokens/stitches.config'
|
||||
import { AnchoredPopover } from '../patterns/AnchoredPopover'
|
||||
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { Button } from '../elements/Button'
|
||||
|
|
|
|||
|
|
@ -12,21 +12,21 @@ type HighlightViewProps = {
|
|||
scrollToHighlight?: (arg: string) => void
|
||||
}
|
||||
|
||||
const StyledQuote = styled(Blockquote, {
|
||||
margin: '0px 0px 0px 0px',
|
||||
fontSize: '18px',
|
||||
lineHeight: '27px',
|
||||
color: '$grayText',
|
||||
padding: '0px 16px',
|
||||
borderLeft: '2px solid $omnivoreCtaYellow',
|
||||
})
|
||||
|
||||
export function HighlightView(props: HighlightViewProps): JSX.Element {
|
||||
const lines = useMemo(
|
||||
() => props.highlight.quote.split('\n'),
|
||||
[props.highlight.quote]
|
||||
)
|
||||
|
||||
const StyledQuote = styled(Blockquote, {
|
||||
margin: '0px 0px 0px 0px',
|
||||
fontSize: '18px',
|
||||
lineHeight: '27px',
|
||||
color: '$grayText',
|
||||
padding: '0px 16px',
|
||||
borderLeft: '2px solid $omnivoreCtaYellow',
|
||||
})
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%', boxSizing: 'border-box' }}>
|
||||
<StyledQuote
|
||||
|
|
@ -49,111 +49,12 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
|
|||
</Fragment>
|
||||
))}
|
||||
</SpanBox>
|
||||
<Box css={{ display: 'block', pt: '16px' }}>
|
||||
{props.highlight.labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</Box>
|
||||
</StyledQuote>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
export function PublicHighlightView(props: HighlightViewProps): JSX.Element {
|
||||
const lines = useMemo(
|
||||
() => props.highlight.quote.split('\n'),
|
||||
[props.highlight.quote]
|
||||
)
|
||||
|
||||
const StyledQuote = styled(Blockquote, {
|
||||
margin: '16px 0px 0px 0px',
|
||||
fontSize: '18px',
|
||||
lineHeight: '1.5',
|
||||
fontFamily: 'Inter',
|
||||
color: '$readerFont',
|
||||
})
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%', bg: '$grayBackground', pt: '0px' }}>
|
||||
<StyledQuote>
|
||||
{props.highlight.prefix}
|
||||
<SpanBox
|
||||
css={{
|
||||
bg: '$omnivoreYellow',
|
||||
p: '1px',
|
||||
borderRadius: '2px',
|
||||
color: '#3d3d3d',
|
||||
}}
|
||||
>
|
||||
{lines.map((line: string, index: number) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
{index !== lines.length - 1 && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</SpanBox>
|
||||
{props.highlight.suffix}
|
||||
</StyledQuote>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightFooterProps = {
|
||||
site: string
|
||||
author?: string
|
||||
title?: string
|
||||
fontSize?: string
|
||||
}
|
||||
|
||||
export function HighlightFooter(props: HighlightFooterProps): JSX.Element {
|
||||
const fontSize = props.fontSize || '12px'
|
||||
|
||||
return (
|
||||
<VStack
|
||||
alignment="start"
|
||||
css={{ width: '100%', bg: '$grayBackground', pt: '0px', pb: '8px' }}
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
fontSize: fontSize,
|
||||
fontFamily: 'Inter',
|
||||
color: '$omnivoreGray',
|
||||
fontWeight: '400',
|
||||
mb: '0px',
|
||||
p: '0px',
|
||||
}}
|
||||
>
|
||||
From{' '}
|
||||
<SpanBox
|
||||
css={{
|
||||
fontSize: fontSize,
|
||||
color: '$omnivoreOrange',
|
||||
p: '0px',
|
||||
fontWeight: '400',
|
||||
mb: '0px',
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
{props.author ? ` — ${props.author}` : null}
|
||||
</SpanBox>
|
||||
</StyledText>
|
||||
<StyledText
|
||||
css={{
|
||||
pt: '0px',
|
||||
mt: '0px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Inter',
|
||||
color: '$omnivoreGray',
|
||||
fontWeight: '400',
|
||||
}}
|
||||
>
|
||||
{props.site}
|
||||
</StyledText>
|
||||
<Box css={{ display: 'block', pt: '16px' }}>
|
||||
{props.highlight.labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</Box>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ export type LinkedItemCardAction =
|
|||
| 'delete'
|
||||
| 'mark-read'
|
||||
| 'mark-unread'
|
||||
| 'share'
|
||||
| 'snooze'
|
||||
| 'set-labels'
|
||||
| 'unsubscribe'
|
||||
| 'update-item'
|
||||
|
|
@ -21,6 +19,5 @@ export type LinkedItemCardProps = {
|
|||
item: LibraryItemNode
|
||||
layout: LayoutType
|
||||
viewer: UserBasicData
|
||||
originText?: string
|
||||
handleAction: (action: LinkedItemCardAction) => void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,190 +0,0 @@
|
|||
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { CoverImage } from '../../elements/CoverImage'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { authoredByText } from '../ArticleSubtitle'
|
||||
import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { CardMenu } from '../CardMenu'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import { ProgressBar } from '../../elements/ProgressBar'
|
||||
import type { LinkedItemCardProps } from './CardTypes'
|
||||
|
||||
export function GridLinkedItemCard(props: LinkedItemCardProps): JSX.Element {
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
p: '$2',
|
||||
pr: '8px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'clip',
|
||||
border: '1px solid $grayBorder',
|
||||
boxShadow: '0px 3px 11px rgba(32, 31, 29, 0.04)',
|
||||
position: 'relative',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onClick={() => {
|
||||
props.handleAction('showDetail')
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
position: 'absolute',
|
||||
top: '1px',
|
||||
left: '1px',
|
||||
width: 'calc(100% - 2px)',
|
||||
'& > div': {
|
||||
borderRadius: '100vmax 100vmax 0 0',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ProgressBar
|
||||
fillPercentage={props.item.readingProgressPercent}
|
||||
fillColor={theme.colors.highlight.toString()}
|
||||
backgroundColor="transparent"
|
||||
borderRadius={
|
||||
props.item.readingProgressPercent === 100 ? '0' : '0px 8px 8px 0px'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<VStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{
|
||||
px: '0px',
|
||||
width: '100%',
|
||||
pl: '$1',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
p: '0px',
|
||||
mr: '-12px',
|
||||
mt: '15px',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 24px',
|
||||
gridTemplateRows: '1fr',
|
||||
}}
|
||||
>
|
||||
<CardTitle title={props.item.title} />
|
||||
<Box
|
||||
css={{ alignSelf: 'end', alignItems: 'start', height: '100%' }}
|
||||
onClick={(e) => {
|
||||
// This is here to prevent menu click events from bubbling
|
||||
// up and causing us to "click" on the link item.
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<CardMenu
|
||||
item={props.item}
|
||||
viewer={props.viewer}
|
||||
triggerElement={
|
||||
<MoreOptionsIcon
|
||||
size={24}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
}
|
||||
actionHandler={props.handleAction}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<HStack alignment="start" distribution="between">
|
||||
<StyledText style="caption" css={{ my: '0', mt: '-$2' }}>
|
||||
{props.item.author && (
|
||||
<SpanBox css={{ mr: '8px' }}>
|
||||
{authoredByText(props.item.author)}
|
||||
</SpanBox>
|
||||
)}
|
||||
<SpanBox css={{ textDecorationLine: 'underline' }}>
|
||||
{props.originText}
|
||||
</SpanBox>
|
||||
</StyledText>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HStack
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
pt: '$2',
|
||||
px: '$1',
|
||||
pr: '12px',
|
||||
mt: '7px',
|
||||
flexGrow: '1',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
m: 0,
|
||||
py: '0px',
|
||||
mr: '$2',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '14px',
|
||||
lineHeight: '125%',
|
||||
color: '$grayTextContrast',
|
||||
flexGrow: '4',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 5,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
data-testid="listDesc"
|
||||
>
|
||||
{props.item.description}
|
||||
</StyledText>
|
||||
{props.item.image && (
|
||||
<CoverImage
|
||||
src={props.item.image}
|
||||
alt="Link Preview Image"
|
||||
width={135}
|
||||
height={90}
|
||||
css={{ ml: '10px', mb: '8px', borderRadius: '3px' }}
|
||||
onError={(e) => {
|
||||
;(e.target as HTMLElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<Box css={{ display: 'block', mt: '8px' }}>
|
||||
{props.item.labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</Box>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type CardTitleProps = {
|
||||
title: string
|
||||
}
|
||||
|
||||
function CardTitle(props: CardTitleProps): JSX.Element {
|
||||
return (
|
||||
<StyledText
|
||||
style="listTitle"
|
||||
data-testid="listTitle"
|
||||
css={{
|
||||
mt: '0',
|
||||
mb: '0',
|
||||
fontWeight: '700',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</StyledText>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import { styled } from '@stitches/react'
|
||||
import { VStack, HStack } from '../../elements/LayoutPrimitives'
|
||||
import { StyledMark, StyledText } from '../../elements/StyledText'
|
||||
import { LinkedItemCardAction, LinkedItemCardProps } from './CardTypes'
|
||||
|
||||
export interface HighlightItemCardProps
|
||||
extends Pick<LinkedItemCardProps, 'item'> {
|
||||
handleAction: (action: LinkedItemCardAction) => void
|
||||
}
|
||||
|
||||
export const PreviewImage = styled('img', {
|
||||
objectFit: 'cover',
|
||||
cursor: 'pointer',
|
||||
})
|
||||
|
||||
export function HighlightItemCard(props: HighlightItemCardProps): JSX.Element {
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
p: '$2',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderRadius: '0px',
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'clip',
|
||||
border: '1px solid $grayBorder',
|
||||
borderBottom: 'none',
|
||||
boxShadow: '0px 3px 11px rgba(32, 31, 29, 0.04)',
|
||||
bg: '$grayBg',
|
||||
'&:focus': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
'&:hover': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onClick={() => {
|
||||
props.handleAction('showDetail')
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
<StyledMark
|
||||
css={{
|
||||
background: 'rgb($highlightBackground)',
|
||||
color: '$highlightText',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{props.item.quote}
|
||||
</StyledMark>
|
||||
</StyledText>
|
||||
<HStack
|
||||
css={{
|
||||
marginTop: 'auto',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{props.item.image && (
|
||||
<PreviewImage
|
||||
src={props.item.image}
|
||||
alt="Preview Image"
|
||||
width={16}
|
||||
height={16}
|
||||
css={{ borderRadius: '50%' }}
|
||||
onError={(e) => {
|
||||
;(e.target as HTMLElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<StyledText
|
||||
css={{
|
||||
fontWeight: '700',
|
||||
}}
|
||||
>
|
||||
{props.item.title}
|
||||
</StyledText>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export const MetaStyle = {
|
||||
width: '100%',
|
||||
color: '$thTextSubtle3',
|
||||
fontSize: '13px',
|
||||
fontWeight: '400',
|
||||
fontFamily: '$display',
|
||||
}
|
||||
|
||||
export const MenuStyle = {
|
||||
display: 'flex',
|
||||
marginLeft: 'auto',
|
||||
height: '30px',
|
||||
width: '30px',
|
||||
mt: '-5px',
|
||||
mr: '-5px',
|
||||
pt: '2px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
'&:hover': {
|
||||
bg: '$thBackground4',
|
||||
},
|
||||
}
|
||||
|
||||
export const TitleStyle = {
|
||||
color: '$thTextContrast2',
|
||||
fontSize: '16px',
|
||||
fontWeight: '700',
|
||||
lineHeight: '1.25',
|
||||
fontFamily: '$display',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
display: '-webkit-box',
|
||||
'-webkit-line-clamp': '2',
|
||||
'-webkit-box-orient': 'vertical',
|
||||
}
|
||||
|
||||
export const DescriptionStyle = {
|
||||
color: '$thTextSubtle',
|
||||
pt: '10px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '400',
|
||||
lineHeight: '140%',
|
||||
fontFamily: '$display',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
'-webkit-line-clamp': '2',
|
||||
'-webkit-box-orient': 'vertical',
|
||||
height: '45px',
|
||||
alignItems: 'start',
|
||||
}
|
||||
|
||||
export const AuthorInfoStyle = {
|
||||
maxLines: '1',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '240px',
|
||||
overflow: 'hidden',
|
||||
height: '21px',
|
||||
color: '$thTextSubtle3',
|
||||
fontSize: '13px',
|
||||
fontWeight: '400',
|
||||
fontFamily: '$display',
|
||||
}
|
||||
|
||||
export const timeAgo = (date: string | undefined): string => {
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
return dayjs(date).fromNow()
|
||||
}
|
||||
|
||||
const shouldHideUrl = (url: string): boolean => {
|
||||
try {
|
||||
const origin = new URL(url).origin
|
||||
const hideHosts = ['https://storage.googleapis.com', 'https://omnivore.app']
|
||||
if (hideHosts.indexOf(origin) != -1) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
console.log('invalid url item', url)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const siteName = (
|
||||
originalArticleUrl: string,
|
||||
itemUrl: string
|
||||
): string => {
|
||||
if (shouldHideUrl(originalArticleUrl)) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
return new URL(originalArticleUrl).hostname.replace(/^www\./, '')
|
||||
} catch {}
|
||||
try {
|
||||
return new URL(itemUrl).hostname.replace(/^www\./, '')
|
||||
} catch {}
|
||||
return ''
|
||||
}
|
||||
|
|
@ -1,229 +1,225 @@
|
|||
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { removeHTMLTags } from '../ArticleSubtitle'
|
||||
import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { CardMenu } from '../CardMenu'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import { ProgressBar } from '../../elements/ProgressBar'
|
||||
import type { LinkedItemCardProps } from './CardTypes'
|
||||
import { ProgressBarVertical } from '../../elements/ProgressBarVertical'
|
||||
import { CoverImage } from '../../elements/CoverImage'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useState } from 'react'
|
||||
import { DotsThreeVertical } from 'phosphor-react'
|
||||
import Link from 'next/link'
|
||||
import { CardMenu } from '../CardMenu'
|
||||
import {
|
||||
AuthorInfoStyle,
|
||||
DescriptionStyle,
|
||||
MenuStyle,
|
||||
MetaStyle,
|
||||
siteName,
|
||||
timeAgo,
|
||||
TitleStyle,
|
||||
} from './LibraryCardStyles'
|
||||
|
||||
//Styles
|
||||
const ellipsisText = {
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 1,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
margin: 'auto 0',
|
||||
pr:'10px',
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
type ProgressBarProps = {
|
||||
fillPercentage: number
|
||||
fillColor: string
|
||||
backgroundColor: string
|
||||
borderRadius: string
|
||||
}
|
||||
|
||||
const cardTitleStyle = {
|
||||
...ellipsisText,
|
||||
width:'100%',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
textAlign: 'left',
|
||||
}
|
||||
|
||||
// Props
|
||||
type CardTitleProps = {
|
||||
title: string
|
||||
}
|
||||
|
||||
// Functions
|
||||
function CardTitle(
|
||||
props: CardTitleProps,
|
||||
): JSX.Element {
|
||||
export function ProgressBar(props: ProgressBarProps): JSX.Element {
|
||||
return (
|
||||
<StyledText style="listTitle" data-testid="listTitle" css={cardTitleStyle}>
|
||||
{props.title}
|
||||
</StyledText>
|
||||
<Box
|
||||
css={{
|
||||
height: '4px',
|
||||
width: '100%',
|
||||
borderRadius: '$1',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: props.backgroundColor,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
height: '100%',
|
||||
width: `${props.fillPercentage}%`,
|
||||
backgroundColor: props.fillColor,
|
||||
borderRadius: props.borderRadius,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Component
|
||||
export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
const originText =
|
||||
props.item.siteName ||
|
||||
siteName(props.item.originalArticleUrl, props.item.url)
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.layout === 'GRID_LAYOUT' ? (
|
||||
<VStack
|
||||
css={{
|
||||
p: '12px',
|
||||
height: '100%',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid $libraryActiveMenuItem',
|
||||
mb: '15px',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onClick={() => {
|
||||
props.handleAction('showDetail')
|
||||
}}
|
||||
<VStack
|
||||
css={{
|
||||
pl: '20px',
|
||||
padding: '15px',
|
||||
width: '320px',
|
||||
height: '100%',
|
||||
minHeight: '270px',
|
||||
background: 'white',
|
||||
borderRadius: '5px',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: '$thBorderColor',
|
||||
cursor: 'pointer',
|
||||
'@smDown': {
|
||||
m: '15px',
|
||||
width: 'calc(100% - 30px)',
|
||||
},
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
|
||||
>
|
||||
<VStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%'
|
||||
...MetaStyle,
|
||||
minHeight: '35px',
|
||||
}}
|
||||
distribution="start"
|
||||
>
|
||||
<HStack
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
<Box>
|
||||
{timeAgo(props.item.savedAt)}
|
||||
{` `}
|
||||
{props.item.wordsCount ?? 0 > 0
|
||||
? ` • ${Math.max(
|
||||
1,
|
||||
Math.round((props.item.wordsCount ?? 0) / 235)
|
||||
)} min read`
|
||||
: null}
|
||||
{props.item.highlights?.length ?? 0 > 0
|
||||
? ` • ${props.item.highlights?.length} highlights`
|
||||
: null}
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 24px',
|
||||
gridTemplateRows: '1fr',
|
||||
width:'100%',
|
||||
...MenuStyle,
|
||||
visibility: isHovered || menuOpen ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardTitle title={props.item.title} />
|
||||
<Box
|
||||
css={{ alignSelf: 'end', alignItems: 'end', height: '100%' }}
|
||||
onClick={(e) => {
|
||||
// This is here to prevent menu click events from bubbling
|
||||
// up and causing us to "click" on the link item.
|
||||
e.stopPropagation()
|
||||
<CardMenu
|
||||
item={props.item}
|
||||
viewer={props.viewer}
|
||||
onOpenChange={(open) => setMenuOpen(open)}
|
||||
actionHandler={props.handleAction}
|
||||
triggerElement={
|
||||
<DotsThreeVertical size={25} weight="bold" color="#ADADAD" />
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
...TitleStyle,
|
||||
height: '42px',
|
||||
}}
|
||||
>
|
||||
{props.item.title}
|
||||
</Box>
|
||||
<Box css={DescriptionStyle}>{props.item.description}</Box>
|
||||
<SpanBox
|
||||
css={{
|
||||
...AuthorInfoStyle,
|
||||
m: '0px',
|
||||
}}
|
||||
>
|
||||
{props.item.author}
|
||||
{props.item.author && originText && ' | '}
|
||||
<SpanBox css={{ textDecoration: 'underline' }}>
|
||||
{originText}
|
||||
</SpanBox>
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
pt: '20px',
|
||||
pr: '10px',
|
||||
pb: '20px',
|
||||
width: '100%',
|
||||
m: '0px',
|
||||
}}
|
||||
>
|
||||
<ProgressBar
|
||||
fillPercentage={props.item.readingProgressPercent}
|
||||
fillColor="$thProgressFg"
|
||||
backgroundColor="$thBorderSubtle"
|
||||
borderRadius="5px"
|
||||
/>
|
||||
</SpanBox>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{ width: '100%', minHeight: '50px' }}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
display: 'block',
|
||||
minHeight: '35px',
|
||||
}}
|
||||
>
|
||||
<CardMenu
|
||||
item={props.item}
|
||||
viewer={props.viewer}
|
||||
triggerElement={
|
||||
<MoreOptionsIcon
|
||||
size={24}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
}
|
||||
actionHandler={props.handleAction}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<HStack alignment="start" distribution="between">
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ fontWeight: '600'}}
|
||||
{props.item.labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</HStack>
|
||||
<VStack
|
||||
css={{
|
||||
width: '80px',
|
||||
height: '100%',
|
||||
marginLeft: 'auto',
|
||||
flexGrow: '1',
|
||||
}}
|
||||
alignment="end"
|
||||
distribution="end"
|
||||
>
|
||||
{props.item.author && (
|
||||
<SpanBox>{removeHTMLTags(props.item.author)}</SpanBox>
|
||||
{props.item.image && (
|
||||
<CoverImage
|
||||
src={props.item.image}
|
||||
alt="Link Preview Image"
|
||||
width={50}
|
||||
height={50}
|
||||
css={{ borderRadius: '8px' }}
|
||||
onError={(e) => {
|
||||
;(e.target as HTMLElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{props.originText && (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
height: '4px',
|
||||
width: '4px',
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block',
|
||||
background: 'var(--colors-graySolid)',
|
||||
}}
|
||||
></Box>
|
||||
<SpanBox css={{ color: 'var(--colors-graySolid)' }}>
|
||||
{props.originText}
|
||||
</SpanBox>
|
||||
</>
|
||||
)}
|
||||
</StyledText>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HStack
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
pr: '12px',
|
||||
flexGrow: '1',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
data-testid="listDesc"
|
||||
>
|
||||
{props.item.description}
|
||||
</StyledText>
|
||||
</HStack>
|
||||
<Box css={{ display: 'block', py: '12px' }}>
|
||||
{props.item.labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</Box>
|
||||
<ProgressBar
|
||||
fillPercentage={props.item.readingProgressPercent}
|
||||
fillColor={theme.colors.highlight.toString()}
|
||||
backgroundColor={theme.colors.lightBorder.toString()}
|
||||
borderRadius={
|
||||
props.item.readingProgressPercent === 100
|
||||
? '0'
|
||||
: '0px 8px 8px 0px'
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
) : (
|
||||
// ELSE display List Layout
|
||||
<VStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '0.01fr 1fr 2fr 150px',
|
||||
gridTemplateRows: '1fr',
|
||||
borderBottom: '1px solid $graySeparator',
|
||||
height: '45px',
|
||||
}}
|
||||
>
|
||||
<ProgressBarVertical
|
||||
fillPercentage={props.item.readingProgressPercent}
|
||||
fillColor={theme.colors.highlight.toString()}
|
||||
backgroundColor={theme.colors.grayProgressBackground.toString()}
|
||||
borderRadius={
|
||||
props.item.readingProgressPercent === 100
|
||||
? '0'
|
||||
: '0px 8px 8px 0px'
|
||||
}
|
||||
height={'45px'}
|
||||
/>
|
||||
<CardTitle title={props.item.title} />
|
||||
<StyledText
|
||||
css={{
|
||||
...ellipsisText,
|
||||
color: '$grayTextContrast',
|
||||
}}
|
||||
data-testid="listDesc"
|
||||
>
|
||||
{props.item.labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
{props.item.description}
|
||||
</StyledText>
|
||||
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ ...ellipsisText, fontWeight: '400' }}
|
||||
>
|
||||
{props.item.author && (
|
||||
<SpanBox>{removeHTMLTags(props.item.author)}</SpanBox>
|
||||
)}
|
||||
</StyledText>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)}
|
||||
</>
|
||||
</a>
|
||||
</Link>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
import { Box, VStack, HStack } from '../../elements/LayoutPrimitives'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { CaretDown, CaretUp } from 'phosphor-react'
|
||||
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
|
||||
import { styled } from '@stitches/react'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { HighlightItem } from '../../templates/homeFeed/HighlightItem'
|
||||
import { getHighlightLocation } from '../../templates/article/NotebookModal'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
|
||||
export const GridSeparator = styled(Box, {
|
||||
height: '1px',
|
||||
marginTop: '15px',
|
||||
backgroundColor: '$thBorderColor',
|
||||
})
|
||||
|
||||
type LibraryHighlightGridCardProps = {
|
||||
viewer: UserBasicData
|
||||
item: LibraryItemNode
|
||||
}
|
||||
|
||||
export function LibraryHighlightGridCard(
|
||||
props: LibraryHighlightGridCardProps
|
||||
): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const higlightCount = props.item.highlights?.length ?? 0
|
||||
|
||||
const sortedHighlights = useMemo(() => {
|
||||
const sorted = (a: number, b: number) => {
|
||||
if (a < b) {
|
||||
return -1
|
||||
}
|
||||
if (a > b) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (!props.item.highlights) {
|
||||
return []
|
||||
}
|
||||
|
||||
return props.item.highlights.sort((a: Highlight, b: Highlight) => {
|
||||
if (a.highlightPositionPercent && b.highlightPositionPercent) {
|
||||
return sorted(a.highlightPositionPercent, b.highlightPositionPercent)
|
||||
}
|
||||
// We do this in a try/catch because it might be an invalid diff
|
||||
// With PDF it will definitely be an invalid diff.
|
||||
try {
|
||||
const aPos = getHighlightLocation(a.patch)
|
||||
const bPos = getHighlightLocation(b.patch)
|
||||
if (aPos && bPos) {
|
||||
return sorted(aPos, bPos)
|
||||
}
|
||||
} catch {}
|
||||
return a.createdAt.localeCompare(b.createdAt)
|
||||
})
|
||||
}, [props.item.highlights])
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
pl: '15px',
|
||||
padding: '15px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
marginTop: '20px',
|
||||
background: 'white',
|
||||
borderRadius: '5px',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: '$thBorderColor',
|
||||
cursor: 'pointer',
|
||||
bg: '$thBackground3',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
{!expanded && (
|
||||
<HStack
|
||||
css={{
|
||||
...MetaStyle,
|
||||
minHeight: '35px',
|
||||
}}
|
||||
distribution="start"
|
||||
>
|
||||
<Box>
|
||||
{timeAgo(props.item.savedAt)}
|
||||
{` `}
|
||||
{props.item.wordsCount ?? 0 > 0
|
||||
? ` • ${Math.max(
|
||||
1,
|
||||
Math.round((props.item.wordsCount ?? 0) / 235)
|
||||
)} min read`
|
||||
: null}
|
||||
{props.item.highlights?.length ?? 0 > 0
|
||||
? ` • ${props.item.highlights?.length} highlights`
|
||||
: null}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
...TitleStyle,
|
||||
}}
|
||||
>
|
||||
{props.item.title}
|
||||
</Box>
|
||||
{expanded && (
|
||||
<>
|
||||
<GridSeparator css={{ width: '100%' }} />
|
||||
<VStack
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
distribution="start"
|
||||
>
|
||||
{sortedHighlights.map((highlight) => (
|
||||
<HighlightItem
|
||||
key={highlight.id}
|
||||
viewer={props.viewer}
|
||||
item={props.item}
|
||||
highlight={highlight}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</>
|
||||
)}
|
||||
<Box css={{ width: '100%' }}>
|
||||
{!expanded ? (
|
||||
<Button
|
||||
css={{
|
||||
mt: '30px',
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
px: '10px',
|
||||
py: '6px',
|
||||
fontSize: '11px',
|
||||
fontFamily: '$inter',
|
||||
fontWeight: '500',
|
||||
bg: '$thBackground2',
|
||||
color: '$thTextSubtle2',
|
||||
alignItems: 'center',
|
||||
border: '1px solid transparent',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
setExpanded(true)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{`View ${higlightCount} highlight${higlightCount > 1 ? 's' : ''}`}
|
||||
<CaretDown
|
||||
size={10}
|
||||
weight="bold"
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
width: '25px',
|
||||
height: '25px',
|
||||
bg: '$thBackground2',
|
||||
pt: '2px',
|
||||
borderRadius: '1000px',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
setExpanded(false)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<CaretUp size={15} weight="bold" color="#EC6A5E" />
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</VStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import type { LinkedItemCardProps } from './CardTypes'
|
||||
import { useState } from 'react'
|
||||
import { DotsThree } from 'phosphor-react'
|
||||
import Link from 'next/link'
|
||||
import { CardMenu } from '../CardMenu'
|
||||
import {
|
||||
AuthorInfoStyle,
|
||||
MenuStyle,
|
||||
MetaStyle,
|
||||
siteName,
|
||||
timeAgo,
|
||||
TitleStyle,
|
||||
} from './LibraryCardStyles'
|
||||
|
||||
export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
const originText =
|
||||
props.item.siteName ||
|
||||
siteName(props.item.originalArticleUrl, props.item.url)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
p: '20px',
|
||||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
gap: '10px',
|
||||
border: '1px solid $grayBorder',
|
||||
borderBottom: 'none',
|
||||
width: '900px',
|
||||
|
||||
'@xxl': {
|
||||
width: '1200px',
|
||||
},
|
||||
|
||||
'@xlgDown': {
|
||||
width: 'unset',
|
||||
borderRadius: 'unset',
|
||||
borderLeft: 'unset',
|
||||
borderRight: 'unset',
|
||||
},
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
|
||||
>
|
||||
<HStack css={MetaStyle} distribution="start">
|
||||
<Box>
|
||||
{timeAgo(props.item.savedAt)}
|
||||
{` `}
|
||||
{props.item.wordsCount ?? 0 > 0
|
||||
? ` • ${Math.max(
|
||||
1,
|
||||
Math.round((props.item.wordsCount ?? 0) / 235)
|
||||
)} min read`
|
||||
: null}
|
||||
{props.item.readingProgressPercent ?? 0 > 0 ? (
|
||||
<>
|
||||
{` • `}
|
||||
<SpanBox css={{ color: '#55B938' }}>
|
||||
{`${Math.round(props.item.readingProgressPercent)}%`}
|
||||
</SpanBox>
|
||||
</>
|
||||
) : null}
|
||||
{props.item.highlights?.length ?? 0 > 0
|
||||
? ` • ${props.item.highlights?.length} highlights`
|
||||
: null}
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
...MenuStyle,
|
||||
visibility: isHovered || menuOpen ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardMenu
|
||||
item={props.item}
|
||||
viewer={props.viewer}
|
||||
onOpenChange={(open) => setMenuOpen(open)}
|
||||
actionHandler={props.handleAction}
|
||||
triggerElement={
|
||||
<DotsThree size={25} weight="bold" color="#ADADAD" />
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<Box css={{ ...TitleStyle, width: '80%' }}>{props.item.title}</Box>
|
||||
<SpanBox
|
||||
css={{
|
||||
mt: '5px',
|
||||
...AuthorInfoStyle,
|
||||
}}
|
||||
>
|
||||
{props.item.author}
|
||||
{props.item.author && originText && ' | '}
|
||||
<SpanBox css={{ textDecoration: 'underline' }}>
|
||||
{originText}
|
||||
</SpanBox>
|
||||
</SpanBox>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
// The two pixels here are to account for the label margin
|
||||
css={{ width: '100%', ml: '-2px', mt: '5px' }}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{props.item.labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</a>
|
||||
</Link>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,46 +1,11 @@
|
|||
import { GridLinkedItemCard } from './GridLinkedItemCard'
|
||||
import { ListLinkedItemCard } from './ListLinkedItemCard'
|
||||
import type { LinkedItemCardProps } from './CardTypes'
|
||||
import { HighlightItemCard } from './HighlightItemCard'
|
||||
import { PageType } from '../../../lib/networking/fragments/articleFragment'
|
||||
|
||||
const shouldHideUrl = (url: string): boolean => {
|
||||
try {
|
||||
const origin = new URL(url).origin
|
||||
const hideHosts = ['https://storage.googleapis.com', 'https://omnivore.app']
|
||||
if (hideHosts.indexOf(origin) != -1) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
console.log('invalid url item', url)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const siteName = (originalArticleUrl: string, itemUrl: string): string => {
|
||||
if (shouldHideUrl(originalArticleUrl)) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
return new URL(originalArticleUrl).hostname.replace(/^www\./, '')
|
||||
} catch {}
|
||||
try {
|
||||
return new URL(itemUrl).hostname.replace(/^www\./, '')
|
||||
} catch {}
|
||||
return ''
|
||||
}
|
||||
import { LibraryGridCard } from './LibraryGridCard'
|
||||
import { LibraryListCard } from './LibraryListCard'
|
||||
|
||||
export function LinkedItemCard(props: LinkedItemCardProps): JSX.Element {
|
||||
const originText =
|
||||
props.item.siteName ||
|
||||
siteName(props.item.originalArticleUrl, props.item.url)
|
||||
|
||||
if (props.item.pageType === PageType.HIGHLIGHTS) {
|
||||
return <HighlightItemCard {...props} />
|
||||
}
|
||||
if (props.layout == 'LIST_LAYOUT') {
|
||||
return <ListLinkedItemCard {...props} originText={originText} />
|
||||
return <LibraryListCard {...props} />
|
||||
} else {
|
||||
return <GridLinkedItemCard {...props} originText={originText} />
|
||||
return <LibraryGridCard {...props} />
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
import {
|
||||
Box,
|
||||
HStack,
|
||||
VStack,
|
||||
MediumBreakpointBox,
|
||||
} from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { authoredByText } from '../ArticleSubtitle'
|
||||
import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { CardMenu } from '../CardMenu'
|
||||
import type { LinkedItemCardProps } from './CardTypes'
|
||||
import { ProgressBar } from '../../elements/ProgressBar'
|
||||
|
||||
export function ListLinkedItemCard(props: LinkedItemCardProps): JSX.Element {
|
||||
return (
|
||||
<MediumBreakpointBox
|
||||
smallerLayoutNode={<ListLinkedItemCardNarrow {...props} />}
|
||||
largerLayoutNode={<ListLinkedItemCardWide {...props} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListLinkedItemCardNarrow(
|
||||
props: LinkedItemCardProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
p: '$3',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
borderRadius: 0,
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
border: '1px solid $grayBorder',
|
||||
borderBottom: 'none',
|
||||
alignItems: 'center',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 24px',
|
||||
gridTemplateRows: '1fr',
|
||||
}}
|
||||
onClick={() => {
|
||||
props.handleAction('showDetail')
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="end"
|
||||
css={{
|
||||
px: '$2',
|
||||
pl: '0px',
|
||||
}}
|
||||
>
|
||||
<VStack>
|
||||
<StyledText
|
||||
style="listTitle"
|
||||
data-testid="listTitle"
|
||||
css={{
|
||||
mt: '0px',
|
||||
mb: '$1',
|
||||
textAlign: 'left',
|
||||
lineHeight: 'normal',
|
||||
}}
|
||||
>
|
||||
{props.item.title}
|
||||
</StyledText>
|
||||
<HStack>
|
||||
{props.item.author && (
|
||||
<StyledText style="caption" css={{ my: '$1' }}>
|
||||
{authoredByText(props.item.author)}
|
||||
</StyledText>
|
||||
)}
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
my: '$1',
|
||||
ml: props.item.author ? '8px' : 0,
|
||||
textDecorationLine: 'underline',
|
||||
}}
|
||||
>
|
||||
{props.originText}
|
||||
</StyledText>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Box
|
||||
css={{
|
||||
alignSelf: 'end',
|
||||
alignItems: 'center',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// This is here to prevent menu click events from bubbling
|
||||
// up and causing us to "click" on the link item.
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<CardMenu
|
||||
item={props.item}
|
||||
viewer={props.viewer}
|
||||
triggerElement={
|
||||
<MoreOptionsIcon
|
||||
size={24}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
}
|
||||
actionHandler={props.handleAction}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListLinkedItemCardWide(
|
||||
props: LinkedItemCardProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
p: '$3',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
borderRadius: 0,
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
border: '1px solid $grayBorder',
|
||||
borderBottom: 'none',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onClick={() => {
|
||||
props.handleAction('showDetail')
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="end"
|
||||
css={{
|
||||
px: '$2',
|
||||
flexGrow: 1,
|
||||
pl: '0px',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
style="listTitle"
|
||||
data-testid="listTitle"
|
||||
css={{ mt: '0px', mb: '$1', textAlign: 'left', lineHeight: 'normal' }}
|
||||
>
|
||||
{props.item.title}
|
||||
</StyledText>
|
||||
{props.item.author && (
|
||||
<StyledText style="caption" css={{ my: '$1', ml: '8px' }}>
|
||||
{authoredByText(props.item.author)}
|
||||
</StyledText>
|
||||
)}
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ my: '$1', ml: '8px', textDecorationLine: 'underline' }}
|
||||
>
|
||||
{props.originText}
|
||||
</StyledText>
|
||||
</HStack>
|
||||
<Box
|
||||
css={{
|
||||
width: '40px',
|
||||
height: '8px',
|
||||
mr: '$2',
|
||||
backgroundColor: '$grayBase',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid $grayBorder',
|
||||
px: '1px',
|
||||
}}
|
||||
>
|
||||
<ProgressBar
|
||||
fillPercentage={props.item.readingProgressPercent}
|
||||
fillColor={theme.colors.highlight.toString()}
|
||||
backgroundColor={theme.colors.grayTextContrast.toString()}
|
||||
borderRadius={'8px'}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
alignSelf: 'end',
|
||||
alignItems: 'center',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// This is here to prevent menu click events from bubbling
|
||||
// up and causing us to "click" on the link item.
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<CardMenu
|
||||
item={props.item}
|
||||
viewer={props.viewer}
|
||||
triggerElement={
|
||||
<MoreOptionsIcon
|
||||
size={24}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
}
|
||||
actionHandler={props.handleAction}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,12 +4,9 @@ import { DropdownMenu, HeaderDropdownAction } from './../patterns/DropdownMenu'
|
|||
import { updateTheme } from '../../lib/themeUpdater'
|
||||
import { AvatarDropdown } from './../elements/AvatarDropdown'
|
||||
import { ThemeId } from './../tokens/stitches.config'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useKeyboardShortcuts } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { setupAnalytics } from '../../lib/analytics'
|
||||
|
||||
type HeaderProps = {
|
||||
user?: UserBasicData
|
||||
|
|
@ -27,34 +24,6 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
|
|||
const router = useRouter()
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
|
||||
useKeyboardShortcuts(
|
||||
primaryCommands((action) => {
|
||||
switch (action) {
|
||||
// case 'themeDarker':
|
||||
// darkenTheme()
|
||||
// break
|
||||
// case 'themeLighter':
|
||||
// lightenTheme()
|
||||
// break
|
||||
case 'toggleShortcutHelpModalDisplay':
|
||||
props.setShowKeyboardCommandsModal(true)
|
||||
break
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const initAnalytics = useCallback(() => {
|
||||
setupAnalytics(props.user)
|
||||
}, [props.user])
|
||||
|
||||
useEffect(() => {
|
||||
initAnalytics()
|
||||
window.addEventListener('load', initAnalytics)
|
||||
return () => {
|
||||
window.removeEventListener('load', initAnalytics)
|
||||
}
|
||||
}, [initAnalytics])
|
||||
|
||||
function headerDropdownActionHandler(action: HeaderDropdownAction): void {
|
||||
switch (action) {
|
||||
case 'apply-darker-theme':
|
||||
|
|
@ -108,12 +77,14 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
|
|||
|
||||
return (
|
||||
<>
|
||||
<SpanBox css={{
|
||||
display: props.alwaysDisplayToolbar ? 'none' : 'flex',
|
||||
'@lgDown': {
|
||||
display: 'none',
|
||||
}
|
||||
}}>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: props.alwaysDisplayToolbar ? 'none' : 'flex',
|
||||
'@lgDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FloatingNavHeader
|
||||
{...props}
|
||||
isVisible={true}
|
||||
|
|
@ -124,11 +95,13 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
|
|||
alwaysDisplayToolbar={props.alwaysDisplayToolbar}
|
||||
/>
|
||||
</SpanBox>
|
||||
<SpanBox css={{
|
||||
'@lg': {
|
||||
display: props.alwaysDisplayToolbar ? 'flex' : 'none',
|
||||
}
|
||||
}}>
|
||||
<SpanBox
|
||||
css={{
|
||||
'@lg': {
|
||||
display: props.alwaysDisplayToolbar ? 'flex' : 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<NavHeader
|
||||
{...props}
|
||||
isVisible={true}
|
||||
|
|
@ -216,10 +189,7 @@ function NavHeader(props: NavHeaderProps): JSX.Element {
|
|||
<DropdownMenu
|
||||
username={props.username}
|
||||
triggerElement={
|
||||
<AvatarDropdown
|
||||
userInitials={props.userInitials}
|
||||
profileImageURL={props.profileImageURL}
|
||||
/>
|
||||
<AvatarDropdown userInitials={props.userInitials} />
|
||||
}
|
||||
actionHandler={props.actionHandler}
|
||||
/>
|
||||
|
|
@ -241,7 +211,7 @@ function FloatingNavHeader(props: NavHeaderProps): JSX.Element {
|
|||
position: 'fixed',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
zIndex: 100,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<OmnivoreNameLogo href={props.username ? '/home' : '/login'} />
|
||||
|
|
@ -256,16 +226,13 @@ function FloatingNavHeader(props: NavHeaderProps): JSX.Element {
|
|||
right: '18px',
|
||||
position: 'fixed',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<DropdownMenu
|
||||
username={props.username}
|
||||
triggerElement={
|
||||
<AvatarDropdown
|
||||
userInitials={props.userInitials}
|
||||
profileImageURL={props.profileImageURL}
|
||||
/>
|
||||
<AvatarDropdown userInitials={props.userInitials} />
|
||||
}
|
||||
actionHandler={props.actionHandler}
|
||||
/>
|
||||
|
|
@ -274,4 +241,3 @@ function FloatingNavHeader(props: NavHeaderProps): JSX.Element {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
39
packages/web/components/patterns/ReaderDropdownMenu.tsx
Normal file
39
packages/web/components/patterns/ReaderDropdownMenu.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { ReactNode } from 'react'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownOption,
|
||||
DropdownSeparator,
|
||||
} from '../elements/DropdownElements'
|
||||
|
||||
type DropdownMenuProps = {
|
||||
triggerElement: ReactNode
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
}
|
||||
|
||||
export function ReaderDropdownMenu(props: DropdownMenuProps): JSX.Element {
|
||||
return (
|
||||
<Dropdown triggerElement={props.triggerElement}>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('archive')}
|
||||
title="Archive"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('editLabels')}
|
||||
title="Edit Labels"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('showEditModal')}
|
||||
title="Edit Info"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('delete')}
|
||||
title="Delete"
|
||||
/>
|
||||
<DropdownSeparator />
|
||||
<DropdownOption
|
||||
onSelect={() => window.Intercom('show')}
|
||||
title="Feedback"
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { Box, VStack, HStack } from '../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { CoverImage } from '../elements/CoverImage'
|
||||
import { ArticleSubtitle } from './ArticleSubtitle'
|
||||
|
||||
|
||||
type ShareArticleViewProps = {
|
||||
url: string
|
||||
title: string
|
||||
imageURL?: string
|
||||
author?: string
|
||||
description?: string
|
||||
originalArticleUrl: string
|
||||
publishedAt: string
|
||||
}
|
||||
|
||||
export function ShareArticleView(props: ShareArticleViewProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
alignment='start'
|
||||
distribution='start'
|
||||
css={{
|
||||
background: 'white',
|
||||
width: '100%',
|
||||
p: '10px 0px 10px 0px',
|
||||
|
||||
boxShadow: 'rgb(120 123 134 / 12%) 0px 4px 18px'
|
||||
}}>
|
||||
{props.imageURL && (
|
||||
<CoverImage
|
||||
src={props.imageURL}
|
||||
alt='Preview Image'
|
||||
width={120}
|
||||
height={120}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<VStack >
|
||||
<Box css={{ pl: '16px', width: '100%', color: 'black', }}>
|
||||
<StyledText style='shareTitle'>{props.title}</StyledText>
|
||||
<ArticleSubtitle
|
||||
style='shareSubtitle'
|
||||
hideButton={true}
|
||||
author={props.author}
|
||||
rawDisplayDate={props.publishedAt}
|
||||
href={props.originalArticleUrl}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { PublicArticleAttributes } from '../../lib/networking/queries/useGetPublicArticleQuery'
|
||||
import { PrimaryLayout } from './PrimaryLayout'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { VStack, HStack, Box } from '../elements/LayoutPrimitives'
|
||||
import { authoredByText } from '../patterns/ArticleSubtitle'
|
||||
import Image from 'next/image'
|
||||
import { HighlightFooter, PublicHighlightView } from '../patterns/HighlightView'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
|
||||
import { Button } from '../elements/Button'
|
||||
|
||||
type ArticleHighlightsProps = {
|
||||
publicArticle: PublicArticleAttributes
|
||||
showAllHighlights: boolean
|
||||
selectedHighlightId?: string
|
||||
previewImagePath?: string
|
||||
}
|
||||
|
||||
export function ArticleHighlights(props: ArticleHighlightsProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<PrimaryLayout
|
||||
pageMetaDataProps={{
|
||||
title: props.publicArticle.title,
|
||||
description: props.publicArticle.description,
|
||||
path: router.pathname,
|
||||
ogImage: props.previewImagePath,
|
||||
ogImageType: 'image/png',
|
||||
}}
|
||||
pageTestId={router.pathname}
|
||||
>
|
||||
<VStack alignment="center" css={{ width: '100%' }}>
|
||||
<LoadedContent
|
||||
publicArticle={props.publicArticle}
|
||||
showAllHighlights={props.showAllHighlights}
|
||||
selectedHighlightId={props.selectedHighlightId}
|
||||
/>
|
||||
</VStack>
|
||||
</PrimaryLayout>
|
||||
)
|
||||
}
|
||||
|
||||
type LoadedContentProps = {
|
||||
publicArticle: PublicArticleAttributes
|
||||
showAllHighlights: boolean
|
||||
selectedHighlightId?: string
|
||||
}
|
||||
|
||||
function LoadedContent(props: LoadedContentProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
const container = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [showAllHighlights, setShowAllHighlights] = useState(
|
||||
props.showAllHighlights
|
||||
)
|
||||
|
||||
const selectedHighlights =
|
||||
props.publicArticle.highlights.filter((highlight) => {
|
||||
return highlight.shortId === props.selectedHighlightId
|
||||
}) ?? ([] as Highlight[])
|
||||
|
||||
const unselectedHighlights =
|
||||
props.publicArticle.highlights.filter((highlight) => {
|
||||
return highlight.shortId !== props.selectedHighlightId
|
||||
}) ?? ([] as Highlight[])
|
||||
|
||||
const moreHighlightsCount = useMemo(() => {
|
||||
return props.publicArticle.highlights.length - 1
|
||||
}, [props.publicArticle.highlights])
|
||||
|
||||
// const sharedBy = useMemo(() => {
|
||||
// if (moreHighlightsCount < 1) return undefined
|
||||
// return props.publicArticle.highlights[0].user
|
||||
// }, [moreHighlightsCount, props.publicArticle.highlights])
|
||||
|
||||
const articleSite = useMemo(() => {
|
||||
try {
|
||||
const url = new URL(props.publicArticle.url)
|
||||
return url.hostname
|
||||
} catch (e) {
|
||||
console.log('error ', e)
|
||||
return ''
|
||||
}
|
||||
}, [props.publicArticle.url])
|
||||
|
||||
return (
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{
|
||||
p: props.selectedHighlightId ? '66px $3 0px $3' : '0px $3 0px $3',
|
||||
width: '66%',
|
||||
maxWidth: '600px',
|
||||
mb: '$4',
|
||||
'@smDown': {
|
||||
p: '32px $3 0px $3',
|
||||
width: '100%',
|
||||
},
|
||||
aspectRatio: router.query.adjustAspectRatio as string,
|
||||
}}
|
||||
ref={container}
|
||||
>
|
||||
{!props.selectedHighlightId && (
|
||||
<LinkedItem publicArticle={props.publicArticle} />
|
||||
)}
|
||||
|
||||
{selectedHighlights.map((highlight) => (
|
||||
<PublicHighlightView
|
||||
key={highlight.id}
|
||||
highlight={highlight}
|
||||
title={props.publicArticle.title}
|
||||
author={props.publicArticle.author}
|
||||
/>
|
||||
))}
|
||||
|
||||
{showAllHighlights &&
|
||||
unselectedHighlights.map((highlight) => (
|
||||
<PublicHighlightView key={highlight.id} highlight={highlight} />
|
||||
))}
|
||||
|
||||
<HighlightFooter
|
||||
title={props.publicArticle.title}
|
||||
author={props.publicArticle.author}
|
||||
site={articleSite}
|
||||
/>
|
||||
|
||||
{!showAllHighlights && moreHighlightsCount > 0 && (
|
||||
<Button onClick={() => setShowAllHighlights(true)}>
|
||||
<Box
|
||||
css={{ width: '8px', color: '$grayBackground', mr: '4px' }}
|
||||
className="dropdown-arrow"
|
||||
/>
|
||||
Read {moreHighlightsCount} more highlight
|
||||
{moreHighlightsCount > 1 ? 's ' : ' '}
|
||||
{/* from {sharedBy.name} */}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ m: '44px', p: '14px 16px', alignSelf: 'center' }}
|
||||
onClick={() => {
|
||||
if (window !== undefined) {
|
||||
window.open(props.publicArticle.url, '_blank')
|
||||
}
|
||||
}}
|
||||
>
|
||||
Read Original
|
||||
</Button>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type LinkedItemProps = {
|
||||
publicArticle: PublicArticleAttributes
|
||||
}
|
||||
|
||||
function LinkedItem(props: LinkedItemProps): JSX.Element {
|
||||
const originText = new URL(props.publicArticle.url).hostname
|
||||
|
||||
return (
|
||||
<VStack
|
||||
alignment="start"
|
||||
css={{
|
||||
bg: '$grayBgActive',
|
||||
borderRadius: '$2',
|
||||
p: '$2',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<HStack distribution="start" css={{ width: '100%' }}>
|
||||
<HStack distribution="start" alignment="start">
|
||||
<Box>
|
||||
<Image
|
||||
src={
|
||||
'/static/images/linked-item-card-placeholder.png'
|
||||
// props.image ?? '/static/images/linked-item-card-placeholder.png'
|
||||
}
|
||||
alt="Link Preview Image"
|
||||
layout="fixed"
|
||||
width={88}
|
||||
height={88}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<VStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{ px: '$2', flexGrow: 1 }}
|
||||
>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ my: '$1', textAlign: 'left', overflow: 'auto' }}
|
||||
>
|
||||
{props.publicArticle.title}
|
||||
</StyledText>
|
||||
{props.publicArticle.author && (
|
||||
<StyledText style="caption" css={{ my: '$1' }}>
|
||||
{authoredByText(props.publicArticle.author)}
|
||||
</StyledText>
|
||||
)}
|
||||
<StyledText style="caption" css={{ my: '$1' }}>
|
||||
{originText}
|
||||
</StyledText>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ m: 0, py: '$2', height: '5em', overflow: 'hidden' }}
|
||||
>
|
||||
{props.publicArticle.description}
|
||||
</StyledText>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -16,9 +16,7 @@ import {
|
|||
primaryCommands,
|
||||
libraryListCommands,
|
||||
highlightBarKeyboardCommands,
|
||||
articleKeyboardCommands,
|
||||
} from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
type KeyboardShortcutListModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
|
|
@ -27,8 +25,6 @@ type KeyboardShortcutListModalProps = {
|
|||
export function KeyboardShortcutListModal(
|
||||
props: KeyboardShortcutListModalProps
|
||||
): JSX.Element {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
|
|
@ -72,10 +68,6 @@ export function KeyboardShortcutListModal(
|
|||
libraryListCommands(() => {})
|
||||
)}
|
||||
/>
|
||||
<ShortcutListSection
|
||||
title="Article"
|
||||
commands={articleKeyboardCommands(router, () => {})}
|
||||
/>
|
||||
<ShortcutListSection
|
||||
title="Highlight Bar"
|
||||
commands={highlightBarKeyboardCommands(() => {})}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
gauthRedirectURI,
|
||||
appleAuthRedirectURI,
|
||||
} from '../../lib/appConfig'
|
||||
import AppleLogin from 'react-apple-login'
|
||||
import { AppleIdButton } from './auth/AppleIdButton'
|
||||
|
||||
export type LoginFormProps = {
|
||||
|
|
|
|||
|
|
@ -81,16 +81,12 @@ function MediumLoginLayout(props: LoginFormProps) {
|
|||
>
|
||||
<LoginForm {...props} />
|
||||
</Box>
|
||||
<OmnivoreIllustration isLargeLayout={true} />
|
||||
<OmnivoreIllustration />
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type OmnivoreIllustrationProps = {
|
||||
isLargeLayout?: boolean
|
||||
}
|
||||
|
||||
function OmnivoreIllustration({ isLargeLayout }: OmnivoreIllustrationProps) {
|
||||
function OmnivoreIllustration() {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
|
|
|
|||
314
packages/web/components/templates/PrimaryDropdown.tsx
Normal file
314
packages/web/components/templates/PrimaryDropdown.tsx
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { Moon, Sun } from 'phosphor-react'
|
||||
import { ReactNode, useCallback } from 'react'
|
||||
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { currentTheme, updateTheme } from '../../lib/themeUpdater'
|
||||
import { Avatar } from '../elements/Avatar'
|
||||
import { AvatarDropdown } from '../elements/AvatarDropdown'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownOption,
|
||||
DropdownSeparator,
|
||||
} from '../elements/DropdownElements'
|
||||
import GridLayoutIcon from '../elements/images/GridLayoutIcon'
|
||||
import ListLayoutIcon from '../elements/images/ListLayoutIcon'
|
||||
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { styled, theme, ThemeId } from '../tokens/stitches.config'
|
||||
import { LayoutType } from './homeFeed/HomeFeedContainer'
|
||||
|
||||
type PrimaryDropdownProps = {
|
||||
children?: ReactNode
|
||||
showThemeSection: boolean
|
||||
|
||||
layout?: LayoutType
|
||||
updateLayout?: (layout: LayoutType) => void
|
||||
}
|
||||
|
||||
export type HeaderDropdownAction =
|
||||
| 'navigate-to-install'
|
||||
| 'navigate-to-emails'
|
||||
| 'navigate-to-labels'
|
||||
| 'navigate-to-profile'
|
||||
| 'navigate-to-subscriptions'
|
||||
| 'navigate-to-api'
|
||||
| 'navigate-to-integrations'
|
||||
| 'increaseFontSize'
|
||||
| 'decreaseFontSize'
|
||||
| 'logout'
|
||||
|
||||
export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const router = useRouter()
|
||||
|
||||
const headerDropdownActionHandler = useCallback(
|
||||
(action: HeaderDropdownAction) => {
|
||||
switch (action) {
|
||||
case 'navigate-to-install':
|
||||
router.push('/settings/installation')
|
||||
break
|
||||
case 'navigate-to-emails':
|
||||
router.push('/settings/emails')
|
||||
break
|
||||
case 'navigate-to-labels':
|
||||
router.push('/settings/labels')
|
||||
break
|
||||
case 'navigate-to-subscriptions':
|
||||
router.push('/settings/subscriptions')
|
||||
break
|
||||
case 'navigate-to-api':
|
||||
router.push('/settings/api')
|
||||
break
|
||||
case 'navigate-to-integrations':
|
||||
router.push('/settings/integrations')
|
||||
break
|
||||
case 'logout':
|
||||
document.dispatchEvent(new Event('logout'))
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
[router]
|
||||
)
|
||||
|
||||
if (!viewerData?.me) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
triggerElement={
|
||||
props.children ?? (
|
||||
<AvatarDropdown userInitials={viewerData?.me?.name.charAt(0) ?? ''} />
|
||||
)
|
||||
}
|
||||
css={{ width: '240px' }}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '64px',
|
||||
p: '15px',
|
||||
gap: '15px',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
imageURL={viewerData.me.profile.pictureUrl}
|
||||
height="40px"
|
||||
fallbackText={viewerData?.me?.name.charAt(0) ?? ''}
|
||||
/>
|
||||
<VStack
|
||||
css={{ height: '40px', maxWidth: '240px' }}
|
||||
alignment="start"
|
||||
distribution="around"
|
||||
>
|
||||
{viewerData.me && (
|
||||
<>
|
||||
<StyledText
|
||||
css={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '$thTextContrast2',
|
||||
m: '0px',
|
||||
p: '0px',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{viewerData.me.name}
|
||||
</StyledText>
|
||||
<StyledText
|
||||
css={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '400',
|
||||
color: '#898989',
|
||||
m: '0px',
|
||||
p: '0px',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{`@${viewerData.me.profile.username}`}
|
||||
</StyledText>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
<DropdownSeparator />
|
||||
{props.showThemeSection && <ThemeSection {...props} />}
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-install')}
|
||||
title="Install"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-emails')}
|
||||
title="Emails"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('navigate-to-labels')}
|
||||
title="Labels"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => window.Intercom('show')}
|
||||
title="Feedback"
|
||||
/>
|
||||
<DropdownSeparator />
|
||||
<DropdownOption
|
||||
onSelect={() => headerDropdownActionHandler('logout')}
|
||||
title="Logout"
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledToggleButton = styled('button', {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '$thTextContrast2',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
width: '70px',
|
||||
height: '100%',
|
||||
borderRadius: '5px',
|
||||
fontSize: '12px',
|
||||
fontFamily: '$inter',
|
||||
gap: '5px',
|
||||
m: '2px',
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
'&[data-state="on"]': {
|
||||
bg: '$thBackground',
|
||||
},
|
||||
})
|
||||
|
||||
function ThemeSection(props: PrimaryDropdownProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<VStack>
|
||||
<HStack
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '100%',
|
||||
px: '15px',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '400',
|
||||
cursor: 'default',
|
||||
color: '$utilityTextDefault',
|
||||
}}
|
||||
>
|
||||
Mode
|
||||
</StyledText>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bg: '$thBackground4',
|
||||
borderRadius: '5px',
|
||||
height: '34px',
|
||||
p: '3px',
|
||||
px: '1px',
|
||||
}}
|
||||
>
|
||||
<StyledToggleButton
|
||||
data-state={currentTheme() != ThemeId.Darker ? 'on' : 'off'}
|
||||
onClick={() => {
|
||||
updateTheme(ThemeId.Light)
|
||||
}}
|
||||
>
|
||||
Light
|
||||
<Sun size={15} color={theme.colors.thTextContrast2.toString()} />
|
||||
</StyledToggleButton>
|
||||
<StyledToggleButton
|
||||
data-state={currentTheme() == ThemeId.Darker ? 'on' : 'off'}
|
||||
onClick={() => {
|
||||
updateTheme(ThemeId.Darker)
|
||||
}}
|
||||
>
|
||||
Dark
|
||||
<Moon size={15} color={theme.colors.thTextContrast2.toString()} />
|
||||
</StyledToggleButton>
|
||||
</Box>
|
||||
</HStack>
|
||||
{props.layout && (
|
||||
<HStack
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '100%',
|
||||
px: '15px',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '400',
|
||||
cursor: 'default',
|
||||
color: '$utilityTextDefault',
|
||||
}}
|
||||
>
|
||||
Layout
|
||||
</StyledText>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bg: '$thBackground4',
|
||||
borderRadius: '5px',
|
||||
height: '34px',
|
||||
p: '3px',
|
||||
px: '1px',
|
||||
}}
|
||||
>
|
||||
<StyledToggleButton
|
||||
data-state={props.layout == 'LIST_LAYOUT' ? 'on' : 'off'}
|
||||
onClick={() => {
|
||||
props.updateLayout && props.updateLayout('LIST_LAYOUT')
|
||||
}}
|
||||
>
|
||||
<ListLayoutIcon
|
||||
color={theme.colors.thTextContrast2.toString()}
|
||||
/>
|
||||
</StyledToggleButton>
|
||||
<StyledToggleButton
|
||||
data-state={props.layout == 'GRID_LAYOUT' ? 'on' : 'off'}
|
||||
onClick={() => {
|
||||
props.updateLayout && props.updateLayout('GRID_LAYOUT')
|
||||
}}
|
||||
>
|
||||
<GridLayoutIcon
|
||||
color={theme.colors.thTextContrast2.toString()}
|
||||
/>
|
||||
</StyledToggleButton>
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
<DropdownSeparator />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,20 +1,15 @@
|
|||
import { PageMetaData, PageMetaDataProps } from '../patterns/PageMetaData'
|
||||
import { Box } from '../elements/LayoutPrimitives'
|
||||
import {
|
||||
ReactNode,
|
||||
MutableRefObject,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { PrimaryHeader } from './../patterns/PrimaryHeader'
|
||||
import { ReactNode, useEffect, useState, useCallback } from 'react'
|
||||
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useKeyboardShortcuts } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Analytics } from '@segment/analytics-next'
|
||||
import { ConfirmationModal } from '../patterns/ConfirmationModal'
|
||||
import { KeyboardShortcutListModal } from './KeyboardShortcutListModal'
|
||||
import { logoutMutation } from '../../lib/networking/mutations/logoutMutation'
|
||||
import { setupAnalytics } from '../../lib/analytics'
|
||||
import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
|
||||
type PrimaryLayoutProps = {
|
||||
children: ReactNode
|
||||
|
|
@ -34,13 +29,25 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
|||
|
||||
useKeyboardShortcuts(navigationCommands(router))
|
||||
|
||||
useKeyboardShortcuts(
|
||||
primaryCommands((action) => {
|
||||
switch (action) {
|
||||
case 'toggleShortcutHelpModalDisplay':
|
||||
setShowKeyboardCommandsModal(true)
|
||||
break
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Attempt to identify the user if they are logged in.
|
||||
useEffect(() => {
|
||||
setupAnalytics(viewerData?.me)
|
||||
|
||||
const user = window.analytics?.user().id()
|
||||
if (!user && viewerData?.me?.id) {
|
||||
window.analytics?.identify({ userId: viewerData?.me?.id })
|
||||
}
|
||||
}, [viewerData?.me?.id])
|
||||
}, [viewerData?.me])
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
await logoutMutation()
|
||||
|
|
@ -56,43 +63,37 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
|||
}
|
||||
}
|
||||
|
||||
const showLogout = useCallback(() => {
|
||||
setShowLogoutConfirmation(true)
|
||||
}, [setShowLogoutConfirmation])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('logout', showLogout)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('logout', showLogout)
|
||||
}
|
||||
}, [showLogout])
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.pageMetaDataProps ? (
|
||||
<PageMetaData {...props.pageMetaDataProps} />
|
||||
) : null}
|
||||
<Box css={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bg: 'transparent',
|
||||
'@smDown': {
|
||||
bg: '$grayBase',
|
||||
}
|
||||
}}>
|
||||
<PrimaryHeader
|
||||
user={viewerData?.me}
|
||||
hideHeader={props.hideHeader}
|
||||
userInitials={viewerData?.me?.name.charAt(0) ?? ''}
|
||||
profileImageURL={viewerData?.me?.profile.pictureUrl}
|
||||
isTransparent={true}
|
||||
toolbarControl={props.headerToolbarControl}
|
||||
alwaysDisplayToolbar={props.alwaysDisplayToolbar}
|
||||
setShowLogoutConfirmation={setShowLogoutConfirmation}
|
||||
setShowKeyboardCommandsModal={setShowKeyboardCommandsModal}
|
||||
/>
|
||||
<Box
|
||||
css={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bg: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
height: '100%',
|
||||
width: '100vw',
|
||||
bg: '$grayBase',
|
||||
bg: '$thBackground2',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
height: '48px',
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
></Box>
|
||||
{props.children}
|
||||
{showLogoutConfirmation ? (
|
||||
<ConfirmationModal
|
||||
|
|
|
|||
|
|
@ -4,14 +4,7 @@ import {
|
|||
ScrollOffsetChangeset,
|
||||
useScrollWatcher,
|
||||
} from '../../../lib/hooks/useScrollWatcher'
|
||||
import {
|
||||
MutableRefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Tweet } from 'react-twitter-widgets'
|
||||
import { render } from 'react-dom'
|
||||
import { isDarkTheme } from '../../../lib/themeUpdater'
|
||||
|
|
@ -97,28 +90,6 @@ export function Article(props: ArticleProps): JSX.Element {
|
|||
}
|
||||
}, 1000)
|
||||
|
||||
const layoutImages = useCallback(
|
||||
(image: HTMLImageElement, container: HTMLDivElement | null) => {
|
||||
if (!container) return
|
||||
const containerWidth = container.clientWidth + 140
|
||||
|
||||
if (!image.closest('blockquote, table')) {
|
||||
let imageWidth = parseFloat(image.getAttribute('width') || '')
|
||||
imageWidth = isNaN(imageWidth) ? image.naturalWidth : imageWidth
|
||||
|
||||
if (imageWidth > containerWidth) {
|
||||
image.style.setProperty(
|
||||
'width',
|
||||
`${Math.min(imageWidth, containerWidth)}px`
|
||||
)
|
||||
image.style.setProperty('max-width', 'unset')
|
||||
image.style.setProperty('margin-left', `-${Math.round(140 / 2)}px`)
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Scroll to initial anchor position
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
|
|
@ -193,22 +164,6 @@ export function Article(props: ArticleProps): JSX.Element {
|
|||
})
|
||||
}, [])
|
||||
|
||||
const onLoadImageHandler = useCallback(() => {
|
||||
const images = articleContentRef.current?.querySelectorAll('img')
|
||||
|
||||
images?.forEach((image) => {
|
||||
layoutImages(image, articleContentRef.current)
|
||||
})
|
||||
}, [layoutImages])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('load', onLoadImageHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('load', onLoadImageHandler)
|
||||
}
|
||||
}, [onLoadImageHandler])
|
||||
|
||||
return (
|
||||
<>
|
||||
<link
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
import { Separator } from '@radix-ui/react-separator'
|
||||
import {
|
||||
ArchiveBox,
|
||||
DotsThree,
|
||||
HighlighterCircle,
|
||||
Info,
|
||||
TagSimple,
|
||||
TextAa,
|
||||
Trash,
|
||||
Tray,
|
||||
} from 'phosphor-react'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Dropdown } from '../../elements/DropdownElements'
|
||||
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { TooltipWrapped } from '../../elements/Tooltip'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { SetLabelsControl } from './SetLabelsControl'
|
||||
import { DisplaySettingsModal } from './DisplaySettingsModal'
|
||||
import { useReaderSettings } from '../../../lib/hooks/useReaderSettings'
|
||||
import { useRef } from 'react'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
|
|
@ -39,40 +35,12 @@ const MenuSeparator = (props: MenuSeparatorProps): JSX.Element => {
|
|||
const LineSeparator = styled(Separator, {
|
||||
width: '100%',
|
||||
margin: 0,
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
borderBottom: `1px solid ${theme.colors.thHighContrast.toString()}`,
|
||||
my: '8px',
|
||||
})
|
||||
return props.layout == 'side' ? <LineSeparator /> : <></>
|
||||
}
|
||||
|
||||
type ActionDropdownProps = {
|
||||
layout: ArticleActionsMenuLayout
|
||||
triggerElement: JSX.Element
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
const ActionDropdown = (props: ActionDropdownProps): JSX.Element => {
|
||||
return (
|
||||
<Dropdown
|
||||
css={{
|
||||
m: '0px',
|
||||
p: '0px',
|
||||
overflow: 'hidden',
|
||||
width: '265px',
|
||||
maxWidth: '265px',
|
||||
'@smDown': { width: '230px' },
|
||||
}}
|
||||
side={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
sideOffset={props.layout == 'side' ? 8 : 0}
|
||||
align={props.layout == 'side' ? 'start' : 'center'}
|
||||
alignOffset={props.layout == 'side' ? -18 : undefined}
|
||||
triggerElement={props.triggerElement}
|
||||
>
|
||||
{props.children}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArticleActionsMenu(
|
||||
props: ArticleActionsMenuProps
|
||||
): JSX.Element {
|
||||
|
|
@ -87,32 +55,15 @@ export function ArticleActionsMenu(
|
|||
alignItems: 'center',
|
||||
flexDirection: props.layout == 'side' ? 'column' : 'row',
|
||||
justifyContent: props.layout == 'side' ? 'center' : 'flex-end',
|
||||
gap: props.layout == 'side' ? '8px' : '24px',
|
||||
gap: props.layout == 'side' ? '15px' : '25px',
|
||||
paddingTop: '6px',
|
||||
}}
|
||||
>
|
||||
{props.showReaderDisplaySettings && (
|
||||
<>
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() =>
|
||||
readerSettings.setShowEditDisplaySettingsModal(true)
|
||||
}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Adjust Display Settings"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<TextAa size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
<MenuSeparator layout={props.layout} />
|
||||
</>
|
||||
)}
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
'@smDown': {
|
||||
alignItems: 'center',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
|
|
@ -130,7 +81,7 @@ export function ArticleActionsMenu(
|
|||
<SpanBox ref={displaySettingsButtonRef}>
|
||||
<TagSimple
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</SpanBox>
|
||||
</TooltipWrapped>
|
||||
|
|
@ -138,36 +89,6 @@ export function ArticleActionsMenu(
|
|||
<MenuSeparator layout={props.layout} />
|
||||
</>
|
||||
) : (
|
||||
// <ActionDropdown
|
||||
// layout={props.layout}
|
||||
// triggerElement={
|
||||
// <TooltipWrapped
|
||||
// tooltipContent="Edit labels"
|
||||
// tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
// >
|
||||
// <TagSimple
|
||||
// size={24}
|
||||
// color={theme.colors.readerFont.toString()}
|
||||
// />
|
||||
// </TooltipWrapped>
|
||||
// }
|
||||
// >
|
||||
// <SetLabelsControl
|
||||
// provider={props.article}
|
||||
// save={(labels: Label[]) => {
|
||||
// if (props.article?.id) {
|
||||
// return setLabelsMutation(
|
||||
// props.article?.id,
|
||||
// labels.map((label) => label.id)
|
||||
// )
|
||||
// }
|
||||
// return Promise.resolve(undefined)
|
||||
// }}
|
||||
// onLabelsChanged={(labels) => {
|
||||
// props.articleActionHandler('refreshLabels', labels)
|
||||
// }}
|
||||
// />
|
||||
// </ActionDropdown>
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
css={{
|
||||
|
|
@ -176,7 +97,10 @@ export function ArticleActionsMenu(
|
|||
},
|
||||
}}
|
||||
>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
<TagSimple
|
||||
size={24}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</SpanBox>
|
||||
|
|
@ -186,17 +110,22 @@ export function ArticleActionsMenu(
|
|||
onClick={() => props.articleActionHandler('setLabels')}
|
||||
css={{
|
||||
display: 'none',
|
||||
'@smDown': {
|
||||
'@mdDown': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
<TagSimple size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('showHighlights')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="View Highlights"
|
||||
|
|
@ -204,11 +133,30 @@ export function ArticleActionsMenu(
|
|||
>
|
||||
<HighlighterCircle
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('showEditModal')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Edit title & description"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<Info size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
<MenuSeparator layout={props.layout} />
|
||||
|
||||
<Button
|
||||
|
|
@ -216,12 +164,19 @@ export function ArticleActionsMenu(
|
|||
onClick={() => {
|
||||
props.articleActionHandler('delete')
|
||||
}}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Delete"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<Trash size={24} color={theme.colors.readerFont.toString()} />
|
||||
<Trash size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
|
|
@ -229,6 +184,10 @@ export function ArticleActionsMenu(
|
|||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('archive')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Archive"
|
||||
|
|
@ -236,7 +195,7 @@ export function ArticleActionsMenu(
|
|||
>
|
||||
<ArchiveBox
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
|
@ -249,7 +208,7 @@ export function ArticleActionsMenu(
|
|||
tooltipContent="Unarchive"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<Tray size={24} color={theme.colors.readerFont.toString()} />
|
||||
<Tray size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -259,16 +218,6 @@ export function ArticleActionsMenu(
|
|||
<DotsThree size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button> */}
|
||||
</Box>
|
||||
{readerSettings.showEditDisplaySettingsModal && (
|
||||
<DisplaySettingsModal
|
||||
centerX={props.layout != 'side'}
|
||||
triggerElementRef={displaySettingsButtonRef}
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
onOpenChange={() =>
|
||||
readerSettings.setShowEditDisplaySettingsModal(false)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.article && readerSettings.showSetLabelsModal && (
|
||||
<SetLabelsModal
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticle
|
|||
import { Article } from './../../../components/templates/article/Article'
|
||||
import { Box, HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives'
|
||||
import { StyledText } from './../../elements/StyledText'
|
||||
import { ArticleSubtitle } from './../../patterns/ArticleSubtitle'
|
||||
import {
|
||||
ArticleSubtitle,
|
||||
ReaderSavedInfo,
|
||||
} from './../../patterns/ArticleSubtitle'
|
||||
import { theme, ThemeId } from './../../tokens/stitches.config'
|
||||
import { HighlightsLayer } from '../../templates/article/HighlightsLayer'
|
||||
import { Button } from '../../elements/Button'
|
||||
|
|
@ -16,6 +19,7 @@ import { LabelChip } from '../../elements/LabelChip'
|
|||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { Avatar } from '../../elements/Avatar'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
|
||||
type ArticleContainerProps = {
|
||||
article: ArticleAttributes
|
||||
|
|
@ -29,8 +33,9 @@ type ArticleContainerProps = {
|
|||
fontFamily?: string
|
||||
lineHeight?: number
|
||||
maxWidthPercentage?: number
|
||||
highContrastFont?: boolean
|
||||
highContrastText?: boolean
|
||||
showHighlightsModal: boolean
|
||||
justifyText?: boolean
|
||||
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +73,7 @@ const RecommendationComments = (
|
|||
</StyledText>
|
||||
</HStack>
|
||||
|
||||
{props.recommendationsWithNotes.map((item, idx) => (
|
||||
{props.recommendationsWithNotes.map((item) => (
|
||||
<VStack
|
||||
key={item.id}
|
||||
alignment="start"
|
||||
|
|
@ -86,7 +91,6 @@ const RecommendationComments = (
|
|||
<Avatar
|
||||
imageURL={item.user?.profileImageURL}
|
||||
height="28px"
|
||||
noFade={true}
|
||||
tooltip={item.user?.name}
|
||||
fallbackText={item.user?.username[0] ?? 'U'}
|
||||
/>
|
||||
|
|
@ -114,8 +118,8 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
const [fontFamilyOverride, setFontFamilyOverride] = useState<string | null>(
|
||||
null
|
||||
)
|
||||
const [highContrastFont, setHighContrastFont] = useState(
|
||||
props.highContrastFont ?? false
|
||||
const [highContrastText, setHighContrastText] = useState(
|
||||
props.highContrastText ?? false
|
||||
)
|
||||
const highlightHref = useRef(
|
||||
window.location.hash ? window.location.hash.split('#')[1] : null
|
||||
|
|
@ -164,7 +168,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
const updateFontFamily = (event: UpdateFontFamilyEvent) => {
|
||||
const newFontFamily =
|
||||
event.fontFamily ?? fontFamilyOverride ?? props.fontFamily ?? 'inter'
|
||||
console.log('setting font fam to', event.fontFamily)
|
||||
setFontFamilyOverride(newFontFamily)
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +177,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
|
||||
const handleFontContrastChange = async (event: UpdateFontContrastEvent) => {
|
||||
const highContrast = event.fontContrast == 'high'
|
||||
setHighContrastFont(highContrast)
|
||||
setHighContrastText(highContrast)
|
||||
}
|
||||
|
||||
interface UpdateFontSizeEvent extends Event {
|
||||
|
|
@ -256,7 +259,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
maxWidthPercentage: maxWidthPercentageOverride ?? props.maxWidthPercentage,
|
||||
lineHeight: lineHeightOverride ?? props.lineHeight ?? 150,
|
||||
fontFamily: fontFamilyOverride ?? props.fontFamily ?? 'inter',
|
||||
readerFontColor: highContrastFont
|
||||
readerFontColor: highContrastText
|
||||
? theme.colors.readerFontHighContrast.toString()
|
||||
: theme.colors.readerFont.toString(),
|
||||
readerTableHeaderColor: theme.colors.readerTableHeader.toString(),
|
||||
|
|
@ -277,10 +280,14 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
id="article-container"
|
||||
css={{
|
||||
padding: '16px',
|
||||
paddingTop: '80px',
|
||||
maxWidth: `${styles.maxWidthPercentage ?? 100}%`,
|
||||
background: props.isAppleAppEmbed
|
||||
? 'unset'
|
||||
: theme.colors.readerBg.toString(),
|
||||
'.article-inner-css': {
|
||||
textAlign: props.justifyText ? 'justify' : 'start',
|
||||
},
|
||||
'--text-font-family': styles.fontFamily,
|
||||
'--text-font-size': `${styles.fontSize}px`,
|
||||
'--line-height': `${styles.lineHeight}%`,
|
||||
|
|
@ -296,7 +303,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
'--blockquote-icon-font-size': '1.7rem',
|
||||
'--figure-margin': '2.6875rem auto',
|
||||
'--hr-margin': '2em',
|
||||
margin: `30px 0px`,
|
||||
margin: `0px 0px`,
|
||||
},
|
||||
'@md': {
|
||||
maxWidth: styles.maxWidthPercentage
|
||||
|
|
@ -306,6 +313,11 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
}}
|
||||
>
|
||||
<VStack alignment="start" distribution="start">
|
||||
<ReaderSavedInfo
|
||||
rawDisplayDate={
|
||||
props.article.publishedAt ?? props.article.createdAt
|
||||
}
|
||||
/>
|
||||
<StyledText
|
||||
style="boldHeadline"
|
||||
data-testid="article-headline"
|
||||
|
|
@ -318,9 +330,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
{props.article.title}
|
||||
</StyledText>
|
||||
<ArticleSubtitle
|
||||
rawDisplayDate={
|
||||
props.article.publishedAt ?? props.article.createdAt
|
||||
}
|
||||
author={props.article.author}
|
||||
href={props.article.url}
|
||||
/>
|
||||
|
|
@ -397,18 +406,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
onOpenChange={(open: boolean) => setShowReportIssuesModal(open)}
|
||||
/>
|
||||
) : null}
|
||||
{/* {showShareModal && (
|
||||
<ShareArticleModal
|
||||
url={`${webBaseURL}/${props.viewerUsername}/${props.article.slug}/highlights?r=true`}
|
||||
title={props.article.title}
|
||||
imageURL={props.article.image}
|
||||
author={props.article.author}
|
||||
publishedAt={props.article.publishedAt ?? props.article.createdAt}
|
||||
description={props.article.description}
|
||||
originalArticleUrl={props.article.originalArticleUrl}
|
||||
onOpenChange={(open: boolean) => setShowShareModal(open)}
|
||||
/>
|
||||
)} */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
import { useCallback } from 'react'
|
||||
import { CommentIcon } from '../../elements/images/CommentIcon'
|
||||
import { CopyLinkIcon } from '../../elements/images/CopyLinkIcon'
|
||||
import { PostIcon } from '../../elements/images/PostIcon'
|
||||
import { ShareIcon } from '../../elements/images/ShareIcon'
|
||||
import { HStack } from './../../elements/LayoutPrimitives'
|
||||
import { useCopyLink } from '../../../lib/hooks/useCopyLink'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { theme } from './../../tokens/stitches.config'
|
||||
import { StyledText } from './../../elements/StyledText'
|
||||
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
|
||||
|
||||
type ArticleHeaderToolbarProps = {
|
||||
articleTitle: string
|
||||
articleShareURL: string
|
||||
hasHighlights: boolean
|
||||
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
setShowShareArticleModal: (showShareModal: boolean) => void
|
||||
}
|
||||
|
||||
export function ArticleHeaderToolbar(
|
||||
props: ArticleHeaderToolbarProps
|
||||
): JSX.Element {
|
||||
const enablePostAction = false // disable for now
|
||||
const { copyLink, isLinkCopied } = useCopyLink(props.articleShareURL, 'link')
|
||||
const canShareNative = useCanShareNative()
|
||||
|
||||
const shareAction = useCallback(() => {
|
||||
if (canShareNative) {
|
||||
navigator
|
||||
?.share({
|
||||
title: props.articleTitle,
|
||||
url: props.articleShareURL,
|
||||
})
|
||||
.then(() => {
|
||||
return
|
||||
})
|
||||
.catch(() => {
|
||||
return
|
||||
})
|
||||
} else {
|
||||
props.setShowShareArticleModal(true)
|
||||
}
|
||||
}, [props, canShareNative])
|
||||
|
||||
return (
|
||||
<HStack distribution="between" alignment="center" css={{ gap: '$2' }}>
|
||||
{props.hasHighlights && (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
onClick={() => {
|
||||
if (props.setShowHighlightsModal) {
|
||||
props.setShowHighlightsModal(true)
|
||||
}
|
||||
}}
|
||||
title="Notebook"
|
||||
>
|
||||
<CommentIcon
|
||||
size={24}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{/* <Button style="plainIcon" onClick={copyLink} title="Copy Link">
|
||||
<CopyLinkIcon
|
||||
isCompleted={isLinkCopied}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
{enablePostAction ? (
|
||||
<Button style="plainIcon" onClick={() => console.log('post action')} title="Post to Profile">
|
||||
<PostIcon
|
||||
isCompleted={false}
|
||||
size={24}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button style="plainIcon" onClick={() => shareAction()} title="Share">
|
||||
<ShareIcon
|
||||
isCompleted={false}
|
||||
size={24}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
{isLinkCopied ? (
|
||||
<StyledText style="caption" css={{ m: 0, p: 0 }}>
|
||||
Link Copied
|
||||
</StyledText>
|
||||
) : null} */}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,31 +1,35 @@
|
|||
import { ReaderSettings } from '../../../lib/hooks/useReaderSettings'
|
||||
import { VStack } from '../../elements/LayoutPrimitives'
|
||||
import {
|
||||
ModalRoot,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { ReaderSettingsControl } from './ReaderSettingsControl'
|
||||
|
||||
|
||||
type DisplaySettingsModalProps = {
|
||||
centerX: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
triggerElementRef?: React.RefObject<HTMLElement>
|
||||
articleActionHandler: (action: string, arg?: number | string) => void
|
||||
readerSettings: ReaderSettings
|
||||
}
|
||||
|
||||
export function DisplaySettingsModal(props: DisplaySettingsModalProps): JSX.Element {
|
||||
const top = props.triggerElementRef?.current?.getBoundingClientRect().bottom ?? 0
|
||||
const left = props.triggerElementRef?.current?.getBoundingClientRect().left ?? 0
|
||||
|
||||
export function DisplaySettingsModal(
|
||||
props: DisplaySettingsModalProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
css={{
|
||||
width: '245px',
|
||||
top: props.triggerElementRef?.current ? top : '50%',
|
||||
left: props.triggerElementRef?.current ? (left - (props.centerX ? 265 / 2 : 0)) : '50%',
|
||||
transform: props.triggerElementRef?.current ? 'unset' : 'translate(-50%, -50%)',
|
||||
width: '345px',
|
||||
padding: '0px',
|
||||
top: '300px',
|
||||
left: 'calc(100% - 250px)',
|
||||
'@lgDown': {
|
||||
top: '300px',
|
||||
left: '50%',
|
||||
},
|
||||
}}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
|
|
@ -33,9 +37,7 @@ export function DisplaySettingsModal(props: DisplaySettingsModalProps): JSX.Elem
|
|||
}}
|
||||
>
|
||||
<VStack css={{ width: '100%' }}>
|
||||
<ReaderSettingsControl
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
/>
|
||||
<ReaderSettingsControl readerSettings={props.readerSettings} />
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { HStack, Box } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { CaretLeft, Check, CheckCircle } from 'phosphor-react'
|
||||
import { CaretLeft, Check } from 'phosphor-react'
|
||||
|
||||
const FONT_FAMILIES = [
|
||||
'Inter',
|
||||
|
|
@ -12,7 +12,7 @@ const FONT_FAMILIES = [
|
|||
'Roboto',
|
||||
'Crimson Text',
|
||||
'OpenDyslexic',
|
||||
'Source Serif Pro'
|
||||
'Source Serif Pro',
|
||||
]
|
||||
|
||||
type FontFamiliesListProps = {
|
||||
|
|
@ -27,18 +27,28 @@ type FontOptionProps = {
|
|||
onSelect: (value: string) => void
|
||||
}
|
||||
|
||||
function FontOption(props: FontOptionProps):JSX.Element {
|
||||
function FontOption(props: FontOptionProps): JSX.Element {
|
||||
const isSelected = props.selected === props.family
|
||||
return (
|
||||
<HStack distribution='between' alignment='start' css={{ width: '100%', pt: '14px' }}>
|
||||
<HStack
|
||||
distribution="between"
|
||||
alignment="start"
|
||||
css={{ width: '100%', pt: '14px' }}
|
||||
>
|
||||
<StyledText
|
||||
css={{ m: '0px', fontSize: 16, fontFamily: props.family, textTransform: 'capitalize', cursor: 'pointer' }}
|
||||
css={{
|
||||
m: '0px',
|
||||
fontSize: 16,
|
||||
fontFamily: props.family,
|
||||
textTransform: 'capitalize',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => props.onSelect(props.family)}
|
||||
>
|
||||
{props.family}
|
||||
</StyledText>
|
||||
{isSelected && (
|
||||
<Check color={theme.colors.grayTextContrast.toString()} weight='bold' />
|
||||
<Check color={theme.colors.grayTextContrast.toString()} weight="bold" />
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
|
|
@ -47,22 +57,50 @@ function FontOption(props: FontOptionProps):JSX.Element {
|
|||
export function FontFamiliesOptions(props: FontFamiliesListProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Box css={{borderBottom: `1px solid ${theme.colors.grayLine.toString()}`, width: '100%'}}>
|
||||
<HStack alignment='center' distribution='between' css={{width: '100%', py: 10, px: 15}}>
|
||||
<Box
|
||||
css={{
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="between"
|
||||
css={{ width: '100%', py: 10, px: 15 }}
|
||||
>
|
||||
<HStack
|
||||
alignment='center'
|
||||
distribution='start'
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ cursor: 'pointer', py: '4px', width: '100%' }}
|
||||
onClick={() => props.setShowFontFamilies(false)}
|
||||
>
|
||||
<CaretLeft color={theme.colors.utilityTextSubtle.toString()} size={15} />
|
||||
<StyledText css={{ textAlign: 'center', m: 0, fontSize: 14, fontWeight: 'bold', width: '100%', wordWrap: 'revert' }}>Choose Font</StyledText>
|
||||
<CaretLeft
|
||||
color={theme.colors.utilityTextSubtle.toString()}
|
||||
size={15}
|
||||
/>
|
||||
<StyledText
|
||||
css={{
|
||||
textAlign: 'center',
|
||||
m: 0,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
width: '100%',
|
||||
wordWrap: 'revert',
|
||||
}}
|
||||
>
|
||||
Choose Font
|
||||
</StyledText>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
<Box css={{ px: 15, pb: 15, width: '100%' }}>
|
||||
{FONT_FAMILIES.map((family) => (
|
||||
<FontOption selected={props.selected} family={family} onSelect={props.onSelect} key={`font-${family}`} />
|
||||
<FontOption
|
||||
selected={props.selected}
|
||||
family={family}
|
||||
onSelect={props.onSelect}
|
||||
key={`font-${family}`}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { HighlightView } from '../../patterns/HighlightView'
|
||||
|
||||
type PageCoordinates = {
|
||||
pageX: number
|
||||
pageY: number
|
||||
}
|
||||
|
||||
type HighlightHoverCardProps = {
|
||||
highlight: Highlight
|
||||
anchorCoordinates: PageCoordinates
|
||||
}
|
||||
|
||||
export function HighlightHoverCard(
|
||||
props: HighlightHoverCardProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
maxWidth: '330px',
|
||||
// height: '48px',
|
||||
position: 'absolute',
|
||||
background: '$grayBg',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid $grayBorder',
|
||||
boxShadow: theme.shadows.cardBoxShadow.toString(),
|
||||
left: props.anchorCoordinates.pageX,
|
||||
top: props.anchorCoordinates.pageY,
|
||||
}}
|
||||
>
|
||||
<HighlightView highlight={props.highlight} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,19 +5,12 @@ import {
|
|||
ModalTitleBar,
|
||||
ModalButtonBar,
|
||||
} from './../../elements/ModalPrimitives'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { CommentIcon } from '../../elements/images/CommentIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { StyledTextArea } from '../../elements/StyledTextArea'
|
||||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { readableUpdatedAtMessage } from './../../../lib/dateFormatting'
|
||||
import { useConfirmListener } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
|
||||
type HighlightNoteModalProps = {
|
||||
author: string
|
||||
|
|
@ -35,18 +28,6 @@ export function HighlightNoteModal(
|
|||
props.highlight?.annotation ?? ''
|
||||
)
|
||||
|
||||
useConfirmListener(
|
||||
() => {
|
||||
saveNoteChanges()
|
||||
},
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
const updatedAtMessage = props.highlight
|
||||
? readableUpdatedAtMessage(props.highlight?.updatedAt)
|
||||
: undefined
|
||||
|
||||
const handleNoteContentChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
setNoteContent(event.target.value)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { HighlightBar, HighlightAction } from '../../patterns/HighlightBar'
|
|||
import { removeHighlights } from '../../../lib/highlights/deleteHighlight'
|
||||
import { createHighlight } from '../../../lib/highlights/createHighlight'
|
||||
import { HighlightNoteModal } from './HighlightNoteModal'
|
||||
import { ShareHighlightModal } from './ShareHighlightModal'
|
||||
import { NotebookModal } from './NotebookModal'
|
||||
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
|
|
@ -69,18 +68,54 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
>([])
|
||||
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 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 canShareNative = useCanShareNative()
|
||||
|
||||
const createHighlightFromSelection = async (
|
||||
selection: SelectionAttributes,
|
||||
note?: string
|
||||
): Promise<Highlight | undefined> => {
|
||||
const result = await createHighlight(
|
||||
{
|
||||
selection: selection,
|
||||
articleId: props.articleId,
|
||||
existingHighlights: highlights,
|
||||
highlightStartEndOffsets: highlightLocations,
|
||||
annotation: note,
|
||||
highlightPositionPercent: selectionPercentPos(selection.selection),
|
||||
highlightPositionAnchorIndex: selectionAnchorIndex(selection.selection),
|
||||
},
|
||||
props.articleMutations
|
||||
)
|
||||
|
||||
if (result.errorMessage) {
|
||||
throw 'Failed to create highlight: ' + result.errorMessage
|
||||
}
|
||||
|
||||
if (!result.highlights || result.highlights.length == 0) {
|
||||
// TODO: show an error message
|
||||
console.error('Failed to create highlight')
|
||||
return undefined
|
||||
}
|
||||
|
||||
setSelectionData(null)
|
||||
setHighlights(result.highlights)
|
||||
|
||||
if (result.newHighlightIndex === undefined) {
|
||||
setHighlightModalAction({ highlightModalAction: 'none' })
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result.highlights[result.newHighlightIndex]
|
||||
}
|
||||
|
||||
// Load the highlights
|
||||
useEffect(() => {
|
||||
const res: HighlightLocation[] = []
|
||||
|
|
@ -105,7 +140,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
anchorElement.scrollIntoView({ behavior: 'auto' })
|
||||
}
|
||||
}
|
||||
}, [highlights, setHighlightLocations])
|
||||
}, [highlights, setHighlightLocations, props.scrollToHighlight])
|
||||
|
||||
const removeHighlightCallback = useCallback(
|
||||
async (id?: string) => {
|
||||
|
|
@ -130,7 +165,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
console.error('Failed to delete highlight')
|
||||
}
|
||||
},
|
||||
[focusedHighlight, highlights, highlightLocations]
|
||||
[focusedHighlight, highlights, highlightLocations, props.articleMutations]
|
||||
)
|
||||
|
||||
const updateHighlightsCallback = useCallback(
|
||||
|
|
@ -224,44 +259,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
return undefined
|
||||
}
|
||||
|
||||
const createHighlightFromSelection = async (
|
||||
selection: SelectionAttributes,
|
||||
note?: string
|
||||
): Promise<Highlight | undefined> => {
|
||||
const result = await createHighlight(
|
||||
{
|
||||
selection: selection,
|
||||
articleId: props.articleId,
|
||||
existingHighlights: highlights,
|
||||
highlightStartEndOffsets: highlightLocations,
|
||||
annotation: note,
|
||||
highlightPositionPercent: selectionPercentPos(selection.selection),
|
||||
highlightPositionAnchorIndex: selectionAnchorIndex(selection.selection),
|
||||
},
|
||||
props.articleMutations
|
||||
)
|
||||
|
||||
if (result.errorMessage) {
|
||||
throw 'Failed to create highlight: ' + result.errorMessage
|
||||
}
|
||||
|
||||
if (!result.highlights || result.highlights.length == 0) {
|
||||
// TODO: show an error message
|
||||
console.error('Failed to create highlight')
|
||||
return undefined
|
||||
}
|
||||
|
||||
setSelectionData(null)
|
||||
setHighlights(result.highlights)
|
||||
|
||||
if (result.newHighlightIndex === undefined) {
|
||||
setHighlightModalAction({ highlightModalAction: 'none' })
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result.highlights[result.newHighlightIndex]
|
||||
}
|
||||
|
||||
const createHighlightCallback = useCallback(
|
||||
async (successAction: HighlightModalAction, annotation?: string) => {
|
||||
if (!selectionData) {
|
||||
|
|
@ -354,7 +351,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
setFocusedHighlight(undefined)
|
||||
}
|
||||
},
|
||||
[highlights, highlightLocations]
|
||||
[highlights, highlightLocations, openNoteModal]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -441,6 +438,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
props.isAppleAppEmbed,
|
||||
removeHighlightCallback,
|
||||
canShareNative,
|
||||
selectionData,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -601,7 +599,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
return (
|
||||
<SetLabelsModal
|
||||
provider={labelsTarget}
|
||||
onOpenChange={function (open: boolean): void {
|
||||
onOpenChange={function (): void {
|
||||
setLabelsTarget(undefined)
|
||||
}}
|
||||
save={function (labels: Label[]): Promise<Label[] | undefined> {
|
||||
|
|
@ -615,23 +613,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
if (
|
||||
highlightModalAction?.highlightModalAction == 'share' &&
|
||||
highlightModalAction.highlight
|
||||
) {
|
||||
return (
|
||||
<ShareHighlightModal
|
||||
url={`${props.highlightsBaseURL}/${highlightModalAction.highlight.shortId}`}
|
||||
title={props.articleTitle}
|
||||
author={props.articleAuthor}
|
||||
highlight={highlightModalAction.highlight}
|
||||
onOpenChange={() => {
|
||||
setHighlightModalAction({ highlightModalAction: 'none' })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Display the button bar if we are not in the native app and there
|
||||
// is a focused highlight or selection data
|
||||
if (!props.highlightBarDisabled && (focusedHighlight || selectionData)) {
|
||||
|
|
|
|||
|
|
@ -4,20 +4,14 @@ import {
|
|||
ModalContent,
|
||||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
VStack,
|
||||
Separator,
|
||||
SpanBox,
|
||||
} from '../../elements/LayoutPrimitives'
|
||||
import { Box, HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { TrashIcon } from '../../elements/images/TrashIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { HighlightView } from '../../patterns/HighlightView'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { StyledTextArea } from '../../elements/StyledTextArea'
|
||||
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
|
||||
import { DotsThree } from 'phosphor-react'
|
||||
|
|
@ -28,6 +22,7 @@ import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabe
|
|||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { diff_match_patch } from 'diff-match-patch'
|
||||
import { HighlightNoteTextEditArea } from '../../elements/HighlightNoteTextEditArea'
|
||||
|
||||
type NotebookModalProps = {
|
||||
highlights: Highlight[]
|
||||
|
|
@ -215,12 +210,12 @@ function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element {
|
|||
{!isEditing ? (
|
||||
<StyledText
|
||||
css={{
|
||||
borderRadius: '6px',
|
||||
bg: '$grayBase',
|
||||
borderRadius: '5px',
|
||||
p: '16px',
|
||||
width: '100%',
|
||||
marginTop: '24px',
|
||||
color: '$grayText',
|
||||
bg: '#EBEBEB',
|
||||
color: '#3D3D3D',
|
||||
}}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
|
|
@ -230,7 +225,7 @@ function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element {
|
|||
</StyledText>
|
||||
) : null}
|
||||
{isEditing && (
|
||||
<TextEditArea
|
||||
<HighlightNoteTextEditArea
|
||||
setIsEditing={setIsEditing}
|
||||
highlight={props.highlight}
|
||||
updateHighlight={props.updateHighlight}
|
||||
|
|
@ -241,86 +236,3 @@ function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type TextEditAreaProps = {
|
||||
setIsEditing: (editing: boolean) => void
|
||||
highlight: Highlight
|
||||
updateHighlight: (highlight: Highlight) => void
|
||||
}
|
||||
|
||||
const TextEditArea = (props: TextEditAreaProps): JSX.Element => {
|
||||
const [noteContent, setNoteContent] = useState(
|
||||
props.highlight.annotation ?? ''
|
||||
)
|
||||
|
||||
const handleNoteContentChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
setNoteContent(event.target.value)
|
||||
},
|
||||
[setNoteContent]
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%' }} key="textEditor">
|
||||
<StyledTextArea
|
||||
css={{
|
||||
my: '$3',
|
||||
minHeight: '$6',
|
||||
borderRadius: '6px',
|
||||
bg: '$grayBase',
|
||||
p: '16px',
|
||||
width: '100%',
|
||||
marginTop: '16px',
|
||||
resize: 'vertical',
|
||||
}}
|
||||
autoFocus
|
||||
maxLength={4000}
|
||||
value={noteContent}
|
||||
placeholder={'Add your notes...'}
|
||||
onChange={handleNoteContentChange}
|
||||
/>
|
||||
<HStack alignment="center" distribution="end" css={{ width: '100%' }}>
|
||||
<Button
|
||||
style="ctaPill"
|
||||
css={{ mr: '$2' }}
|
||||
onClick={() => {
|
||||
props.setIsEditing(false)
|
||||
setNoteContent(props.highlight.annotation ?? '')
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
console.log('updating highlight')
|
||||
try {
|
||||
const result = await updateHighlightMutation({
|
||||
highlightId: props.highlight.id,
|
||||
annotation: noteContent,
|
||||
})
|
||||
console.log('result: ' + result)
|
||||
|
||||
if (!result) {
|
||||
showErrorToast('There was an error updating your highlight.')
|
||||
} else {
|
||||
showSuccessToast('Note saved')
|
||||
props.highlight.annotation = noteContent
|
||||
props.updateHighlight(props.highlight)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error updating annoation', err)
|
||||
showErrorToast('There was an error updating your highlight.')
|
||||
}
|
||||
|
||||
props.setIsEditing(false)
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,7 @@ import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticle
|
|||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef,
|
||||
ReactComponentElement,
|
||||
} from 'react'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { isDarkTheme } from '../../../lib/themeUpdater'
|
||||
import PSPDFKit from 'pspdfkit'
|
||||
import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit'
|
||||
|
|
@ -17,7 +11,6 @@ import { createHighlightMutation } from '../../../lib/networking/mutations/creat
|
|||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { ShareHighlightModal } from './ShareHighlightModal'
|
||||
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
|
||||
import { webBaseURL } from '../../../lib/appConfig'
|
||||
import { pspdfKitKey } from '../../../lib/appConfig'
|
||||
|
|
@ -36,14 +29,12 @@ export default function PdfArticleContainer(
|
|||
props: PdfArticleContainerProps
|
||||
): JSX.Element {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const [shareTarget, setShareTarget] = useState<Highlight | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [shareTarget, setShareTarget] =
|
||||
useState<Highlight | undefined>(undefined)
|
||||
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
|
||||
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] = useState<
|
||||
number | undefined
|
||||
>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] =
|
||||
useState<number | undefined>(undefined)
|
||||
const highlightsRef = useRef<Highlight[]>([])
|
||||
const canShareNative = useCanShareNative()
|
||||
|
||||
|
|
@ -465,17 +456,6 @@ export default function PdfArticleContainer(
|
|||
return (
|
||||
<Box css={{ width: '100%', height: '100%' }}>
|
||||
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
|
||||
{shareTarget && (
|
||||
<ShareHighlightModal
|
||||
url={getHighlightURL(shareTarget.shortId)}
|
||||
title={props.article.title}
|
||||
author={props.article.author}
|
||||
highlight={shareTarget}
|
||||
onOpenChange={() => {
|
||||
setShareTarget(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{noteTarget && (
|
||||
<HighlightNoteModal
|
||||
highlight={noteTarget}
|
||||
|
|
|
|||
|
|
@ -1,284 +1,605 @@
|
|||
import { HStack, VStack, SpanBox, Box } from '../../elements/LayoutPrimitives'
|
||||
import { HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { styled, theme, ThemeId } from '../../tokens/stitches.config'
|
||||
import {
|
||||
AlignCenterHorizontalSimple,
|
||||
ArrowsHorizontal,
|
||||
ArrowsInLineHorizontal,
|
||||
ArrowsOutLineHorizontal,
|
||||
CaretLeft,
|
||||
CaretRight,
|
||||
Cursor,
|
||||
Check,
|
||||
} from 'phosphor-react'
|
||||
import { TickedRangeSlider } from '../../elements/TickedRangeSlider'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { FontFamiliesOptions } from './FontFamiliesOptions'
|
||||
import { useReaderSettings } from '../../../lib/hooks/useReaderSettings'
|
||||
import { ReaderSettings } from '../../../lib/hooks/useReaderSettings'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { currentThemeName, updateTheme } from '../../../lib/themeUpdater'
|
||||
import { LineHeightIncreaseIcon } from '../../elements/images/LineHeightIncreaseIconProps'
|
||||
import { LineHeightDecreaseIcon } from '../../elements/images/LineHeightDecreaseIcon'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
|
||||
type ReaderSettingsProps = {
|
||||
articleActionHandler: (action: string, arg?: number | string) => void
|
||||
readerSettings: ReaderSettings
|
||||
}
|
||||
|
||||
const VerticalDivider = styled(SpanBox, {
|
||||
width: '1px',
|
||||
height: '100%',
|
||||
background: `${theme.colors.grayLine.toString()}`,
|
||||
})
|
||||
|
||||
const HorizontalDivider = styled(SpanBox, {
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: `${theme.colors.grayLine.toString()}`,
|
||||
})
|
||||
|
||||
const FONT_FAMILIES = [
|
||||
'Inter',
|
||||
'System Default',
|
||||
'Merriweather',
|
||||
'Lora',
|
||||
'Open Sans',
|
||||
'Roboto',
|
||||
'Crimson Text',
|
||||
'OpenDyslexic',
|
||||
'Source Serif Pro',
|
||||
]
|
||||
|
||||
type SettingsProps = {
|
||||
readerSettings: ReaderSettings
|
||||
setShowAdvanced: (show: boolean) => void
|
||||
}
|
||||
|
||||
export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element {
|
||||
const [showFontOptions, setShowFontOptions] = useState(false)
|
||||
const readerSettings = useReaderSettings()
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%' }}>
|
||||
{showFontOptions ? (
|
||||
<FontFamiliesOptions
|
||||
selected={readerSettings.fontFamily}
|
||||
setShowFontFamilies={setShowFontOptions}
|
||||
onSelect={(font: string) => {
|
||||
readerSettings.setFontFamily(font)
|
||||
props.articleActionHandler('setFontFamily', font)
|
||||
}}
|
||||
<>
|
||||
{showAdvanced ? (
|
||||
<AdvancedSettings
|
||||
readerSettings={props.readerSettings}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '44px',
|
||||
verticalAlign: 'baseline',
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ width: '50%', fontSize: '32px' }}
|
||||
onClick={() => props.articleActionHandler('decrementFontSize')}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<VerticalDivider />
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ width: '50%', height: '100%', fontSize: '32px' }}
|
||||
onClick={() => props.articleActionHandler('incrementFontSize')}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</HStack>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
m: '0px',
|
||||
px: '12px',
|
||||
py: '12px',
|
||||
width: '100%',
|
||||
height: '44px',
|
||||
verticalAlign: 'baseline',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setShowFontOptions(true)}
|
||||
>
|
||||
<StyledText css={{ m: '0px' }}>Font:</StyledText>
|
||||
<StyledText
|
||||
css={{
|
||||
m: '0px',
|
||||
marginLeft: 'auto',
|
||||
pr: '4px',
|
||||
fontFamily: readerSettings.fontFamily,
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{readerSettings.fontFamily}
|
||||
</StyledText>
|
||||
<CaretRight
|
||||
width={16}
|
||||
height={16}
|
||||
color={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</HStack>
|
||||
<HorizontalDivider
|
||||
css={{
|
||||
'@smDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<VStack
|
||||
css={{
|
||||
p: '0px',
|
||||
m: '0px',
|
||||
pb: '14px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'@smDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
color={theme.colors.readerFont.toString()}
|
||||
css={{ pl: '12px', m: '0px', pt: '14px' }}
|
||||
>
|
||||
Margin:
|
||||
</StyledText>
|
||||
<HStack
|
||||
distribution="between"
|
||||
css={{ gap: '16px', alignItems: 'center', alignSelf: 'center' }}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ pt: '10px', pl: '12px' }}
|
||||
onClick={() => {
|
||||
const newMarginWith = Math.max(
|
||||
readerSettings.marginWidth - 45,
|
||||
200
|
||||
)
|
||||
readerSettings.setMarginWidth(newMarginWith)
|
||||
props.articleActionHandler('setMarginWidth', newMarginWith)
|
||||
}}
|
||||
>
|
||||
<ArrowsOutLineHorizontal
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</Button>
|
||||
<TickedRangeSlider
|
||||
min={200}
|
||||
max={560}
|
||||
step={45}
|
||||
value={readerSettings.marginWidth}
|
||||
onChange={(value) => {
|
||||
readerSettings.setMarginWidth(value)
|
||||
props.articleActionHandler('setMarginWidth', value)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ pt: '10px', pr: '12px' }}
|
||||
onClick={() => {
|
||||
const newMarginWith = Math.min(
|
||||
readerSettings.marginWidth + 45,
|
||||
560
|
||||
)
|
||||
readerSettings.setMarginWidth(newMarginWith)
|
||||
props.articleActionHandler('setMarginWidth', newMarginWith)
|
||||
}}
|
||||
>
|
||||
<ArrowsInLineHorizontal
|
||||
size={24}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HorizontalDivider />
|
||||
|
||||
<VStack
|
||||
css={{
|
||||
p: '0px',
|
||||
m: '0px',
|
||||
pb: '14px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
color={theme.colors.readerFont.toString()}
|
||||
css={{ pl: '12px', m: '0px', pt: '14px' }}
|
||||
>
|
||||
Line Spacing:
|
||||
</StyledText>
|
||||
<HStack
|
||||
distribution="between"
|
||||
css={{ gap: '16px', alignItems: 'center', alignSelf: 'center' }}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ pt: '10px', pl: '12px' }}
|
||||
onClick={() => {
|
||||
const newLineHeight = Math.max(
|
||||
readerSettings.lineHeight - 25,
|
||||
100
|
||||
)
|
||||
readerSettings.setLineHeight(newLineHeight)
|
||||
props.articleActionHandler('setLineHeight', newLineHeight)
|
||||
}}
|
||||
>
|
||||
<AlignCenterHorizontalSimple
|
||||
size={25}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</Button>
|
||||
<TickedRangeSlider
|
||||
min={100}
|
||||
max={300}
|
||||
step={25}
|
||||
value={readerSettings.lineHeight}
|
||||
onChange={(value) => {
|
||||
readerSettings.setLineHeight(value)
|
||||
props.articleActionHandler('setLineHeight', value)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ pt: '10px', pr: '12px' }}
|
||||
onClick={() => {
|
||||
const newLineHeight = Math.min(
|
||||
readerSettings.lineHeight + 25,
|
||||
300
|
||||
)
|
||||
readerSettings.setLineHeight(newLineHeight)
|
||||
props.articleActionHandler('setLineHeight', newLineHeight)
|
||||
}}
|
||||
>
|
||||
<AlignCenterHorizontalSimple
|
||||
size={25}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HorizontalDivider />
|
||||
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
justifyContent: 'center',
|
||||
textDecoration: 'underline',
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
width: '100%',
|
||||
fontSize: '12px',
|
||||
p: '12px',
|
||||
pb: '14px',
|
||||
pt: '16px',
|
||||
height: '42px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onClick={() => {
|
||||
readerSettings.setFontFamily('Inter')
|
||||
readerSettings.setMarginWidth(290)
|
||||
readerSettings.setLineHeight(150)
|
||||
props.articleActionHandler('resetReaderSettings')
|
||||
showSuccessToast('Display settings reset', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Reset to default
|
||||
</Button>
|
||||
</>
|
||||
<BasicSettings
|
||||
readerSettings={props.readerSettings}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AdvancedSettings(props: SettingsProps): JSX.Element {
|
||||
return (
|
||||
<VStack
|
||||
css={{ width: '100%', minHeight: '320px', p: '10px' }}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
m: '10px',
|
||||
mb: '20px',
|
||||
display: 'flex',
|
||||
fontSize: '12px',
|
||||
fontWeight: '400',
|
||||
fontFamily: '$display',
|
||||
alignItems: 'center',
|
||||
borderRadius: '6px',
|
||||
gap: '5px',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setShowAdvanced(false)
|
||||
}}
|
||||
>
|
||||
<CaretLeft size={12} color="#969696" weight="bold" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
pr: '30px',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
'&[data-state="on"]': {
|
||||
bg: '$thBackground',
|
||||
},
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
>
|
||||
<Label htmlFor="justify-text" css={{ width: '100%' }}>
|
||||
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
|
||||
Justify Text
|
||||
</StyledText>
|
||||
</Label>
|
||||
<SwitchRoot
|
||||
id="justify-text"
|
||||
checked={props.readerSettings.justifyText ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
props.readerSettings.setJustifyText(checked)
|
||||
}}
|
||||
>
|
||||
<SwitchThumb />
|
||||
</SwitchRoot>
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
pr: '30px',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
'&[data-state="on"]': {
|
||||
bg: '$thBackground',
|
||||
},
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
>
|
||||
<Label htmlFor="high-contrast-text" css={{ width: '100%' }}>
|
||||
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
|
||||
High Contrast Text
|
||||
</StyledText>
|
||||
</Label>
|
||||
<SwitchRoot
|
||||
id="high-contrast-text"
|
||||
checked={props.readerSettings.highContrastText ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
props.readerSettings.setHighContrastText(checked)
|
||||
}}
|
||||
>
|
||||
<SwitchThumb />
|
||||
</SwitchRoot>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const SwitchRoot = styled(Switch.Root, {
|
||||
all: 'unset',
|
||||
width: 42,
|
||||
height: 25,
|
||||
backgroundColor: '$thBorderColor',
|
||||
borderRadius: '9999px',
|
||||
position: 'relative',
|
||||
WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)',
|
||||
'&:focus': { boxShadow: `0 0 0 2px $thBorderColor` },
|
||||
'&[data-state="checked"]': { backgroundColor: '$thBorderColor' },
|
||||
})
|
||||
|
||||
const SwitchThumb = styled(Switch.Thumb, {
|
||||
display: 'block',
|
||||
width: 21,
|
||||
height: 21,
|
||||
backgroundColor: '$thTextContrast2',
|
||||
borderRadius: '9999px',
|
||||
transition: 'transform 100ms',
|
||||
transform: 'translateX(2px)',
|
||||
willChange: 'transform',
|
||||
'&[data-state="checked"]': { transform: 'translateX(19px)' },
|
||||
})
|
||||
|
||||
const Label = styled('label', {
|
||||
color: 'white',
|
||||
fontSize: 15,
|
||||
lineHeight: 1,
|
||||
})
|
||||
|
||||
function BasicSettings(props: SettingsProps): JSX.Element {
|
||||
return (
|
||||
<VStack css={{ width: '100%' }}>
|
||||
<FontControls readerSettings={props.readerSettings} />
|
||||
|
||||
<HorizontalDivider />
|
||||
|
||||
<LayoutControls readerSettings={props.readerSettings} />
|
||||
|
||||
<HorizontalDivider />
|
||||
|
||||
<ThemeSelector {...props} />
|
||||
|
||||
<HorizontalDivider />
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ width: '100%', p: '10px' }}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
pl: '10px',
|
||||
display: 'flex',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
fontFamily: '$display',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
props.readerSettings.setFontFamily('Inter')
|
||||
props.readerSettings.setMarginWidth(290)
|
||||
props.readerSettings.setLineHeight(150)
|
||||
props.readerSettings.actionHandler('resetReaderSettings')
|
||||
showSuccessToast('Display settings reset', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
p: '5px',
|
||||
display: 'flex',
|
||||
fontSize: '12px',
|
||||
fontWeight: '400',
|
||||
fontFamily: '$display',
|
||||
marginLeft: 'auto',
|
||||
alignItems: 'center',
|
||||
borderRadius: '6px',
|
||||
gap: '5px',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setShowAdvanced(true)
|
||||
}}
|
||||
>
|
||||
Advanced Settings
|
||||
<CaretRight size={12} color="#969696" weight="bold" />
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type FontControlsProps = {
|
||||
readerSettings: ReaderSettings
|
||||
}
|
||||
|
||||
function FontControls(props: FontControlsProps): JSX.Element {
|
||||
const FontSelect = styled('select', {
|
||||
pl: '5px',
|
||||
height: '30px',
|
||||
minWidth: '100px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '12px',
|
||||
background: '$thBackground',
|
||||
border: '1px solid $thBorderColor',
|
||||
fontFamily: props.readerSettings.fontFamily,
|
||||
textTransform: 'capitalize',
|
||||
borderRadius: '4px',
|
||||
})
|
||||
|
||||
const handleFontSizeChange = useCallback(
|
||||
(value) => {
|
||||
props.readerSettings.actionHandler('setFontSize', value)
|
||||
},
|
||||
[props.readerSettings.actionHandler]
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%', pb: '20px' }}>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{ width: '100%', p: '20px', pb: '10px' }}
|
||||
>
|
||||
<StyledText style="displaySettingsLabel">Font</StyledText>
|
||||
<FontSelect
|
||||
css={{ marginLeft: 'auto' }}
|
||||
tabIndex={-1}
|
||||
defaultValue={props.readerSettings.fontFamily}
|
||||
onChange={(e: React.FormEvent<HTMLSelectElement>) => {
|
||||
const font = e.currentTarget.value
|
||||
if (FONT_FAMILIES.indexOf(font) < 0) {
|
||||
return
|
||||
}
|
||||
props.readerSettings.setFontFamily(font)
|
||||
}}
|
||||
>
|
||||
{FONT_FAMILIES.map((family) => (
|
||||
<option key={`font-${family}`} value={family}>
|
||||
{family}
|
||||
</option>
|
||||
))}
|
||||
</FontSelect>
|
||||
</HStack>
|
||||
<HStack
|
||||
css={{ px: '10px', width: '100%' }}
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ py: '0px', width: '60px' }}
|
||||
onClick={() => {
|
||||
props.readerSettings.actionHandler('decrementFontSize')
|
||||
}}
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
fontFamily: '$display',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '20px',
|
||||
lineHeight: '20px',
|
||||
textAlign: 'center',
|
||||
color: '#969696',
|
||||
width: '50px',
|
||||
pr: '5px',
|
||||
pb: '5px',
|
||||
}}
|
||||
>
|
||||
a
|
||||
</SpanBox>
|
||||
</Button>
|
||||
<TickedRangeSlider
|
||||
min={10}
|
||||
max={34}
|
||||
step={2}
|
||||
value={props.readerSettings.fontSize}
|
||||
onChange={handleFontSizeChange}
|
||||
/>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ py: '0px', width: '60px' }}
|
||||
onClick={() => {
|
||||
props.readerSettings.actionHandler('incrementFontSize')
|
||||
}}
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
fontFamily: '$display',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '20px',
|
||||
lineHeight: '20px',
|
||||
|
||||
textAlign: 'center',
|
||||
|
||||
color: '#969696',
|
||||
width: '60px',
|
||||
pl: '16px',
|
||||
}}
|
||||
>
|
||||
A
|
||||
</SpanBox>
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type LayoutControlsProps = {
|
||||
readerSettings: ReaderSettings
|
||||
}
|
||||
|
||||
function LayoutControls(props: LayoutControlsProps): JSX.Element {
|
||||
const handleMarginWidthChange = useCallback(
|
||||
(value) => {
|
||||
props.readerSettings.setMarginWidth(value)
|
||||
},
|
||||
[props.readerSettings.actionHandler, props.readerSettings.setMarginWidth]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack
|
||||
css={{
|
||||
m: '0px',
|
||||
px: '0px',
|
||||
pb: '10px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
|
||||
Margin
|
||||
</StyledText>
|
||||
<HStack
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ py: '0px', width: '60px' }}
|
||||
onClick={() => {
|
||||
const newMarginWith = Math.max(
|
||||
props.readerSettings.marginWidth - 45,
|
||||
200
|
||||
)
|
||||
props.readerSettings.setMarginWidth(newMarginWith)
|
||||
}}
|
||||
>
|
||||
<ArrowsHorizontal size={24} color="#969696" />
|
||||
</Button>
|
||||
<TickedRangeSlider
|
||||
min={200}
|
||||
max={560}
|
||||
step={45}
|
||||
value={props.readerSettings.marginWidth}
|
||||
onChange={handleMarginWidthChange}
|
||||
/>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ py: '0px', width: '60px' }}
|
||||
onClick={() => {
|
||||
const newMarginWith = Math.min(
|
||||
props.readerSettings.marginWidth + 45,
|
||||
560
|
||||
)
|
||||
props.readerSettings.setMarginWidth(newMarginWith)
|
||||
}}
|
||||
>
|
||||
<ArrowsInLineHorizontal size={24} color="#969696" />
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
<VStack
|
||||
css={{
|
||||
m: '0px',
|
||||
px: '0px',
|
||||
pb: '20px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
|
||||
Line Height
|
||||
</StyledText>
|
||||
<HStack
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ py: '0px', width: '60px' }}
|
||||
onClick={() => {
|
||||
const newLineHeight = Math.max(
|
||||
props.readerSettings.lineHeight - 25,
|
||||
100
|
||||
)
|
||||
props.readerSettings.setLineHeight(newLineHeight)
|
||||
}}
|
||||
>
|
||||
<LineHeightDecreaseIcon
|
||||
strokeColor={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</Button>
|
||||
<TickedRangeSlider
|
||||
min={100}
|
||||
max={300}
|
||||
step={25}
|
||||
value={props.readerSettings.lineHeight}
|
||||
onChange={(value) => {
|
||||
props.readerSettings.setLineHeight(value)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ py: '0px', width: '60px' }}
|
||||
onClick={() => {
|
||||
const newLineHeight = Math.min(
|
||||
props.readerSettings.lineHeight + 25,
|
||||
300
|
||||
)
|
||||
props.readerSettings.setLineHeight(newLineHeight)
|
||||
}}
|
||||
>
|
||||
<LineHeightIncreaseIcon
|
||||
strokeColor={theme.colors.readerFont.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ThemeSelector(props: ReaderSettingsProps): JSX.Element {
|
||||
const [currentTheme, setCurrentTheme] = useState(currentThemeName())
|
||||
|
||||
const isDark = useMemo(() => {
|
||||
return currentTheme === 'Dark' || currentTheme === 'Darker'
|
||||
}, [currentTheme])
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
px: '20px',
|
||||
m: '0px',
|
||||
pb: '10px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<StyledText style="displaySettingsLabel">Themes</StyledText>
|
||||
<HStack
|
||||
distribution="start"
|
||||
css={{
|
||||
gap: '16px',
|
||||
pl: '5px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="themeSwitch"
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
background: '#F5F5F5',
|
||||
borderRadius: '50%',
|
||||
border: 'unset',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)',
|
||||
border: '2px solid #6A6968',
|
||||
},
|
||||
'&[data-state="selected"]': {
|
||||
border: '2px solid #6A6968',
|
||||
},
|
||||
}}
|
||||
data-state={isDark ? 'unselected' : 'selected'}
|
||||
onClick={() => {
|
||||
updateTheme(ThemeId.Light)
|
||||
setCurrentTheme(currentThemeName())
|
||||
}}
|
||||
>
|
||||
{!isDark && <Check color="#6A6968" size={15} weight="bold" />}
|
||||
</Button>
|
||||
<Button
|
||||
style="themeSwitch"
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
background: '#3B3938',
|
||||
borderRadius: '50%',
|
||||
border: 'unset',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)',
|
||||
border: '2px solid #6A6968',
|
||||
},
|
||||
'&[data-state="selected"]': {
|
||||
border: '2px solid #6A6968',
|
||||
},
|
||||
}}
|
||||
data-state={isDark ? 'selected' : 'unselected'}
|
||||
onClick={() => {
|
||||
updateTheme(ThemeId.Dark)
|
||||
setCurrentTheme(currentThemeName())
|
||||
}}
|
||||
>
|
||||
{isDark && <Check color="#F9D354" size={20} />}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
|
||||
revalidate()
|
||||
},
|
||||
[isSelected, props]
|
||||
[isSelected, props, revalidate]
|
||||
)
|
||||
|
||||
const filteredLabels = useMemo(() => {
|
||||
|
|
@ -295,6 +295,23 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const createLabelFromFilterText = useCallback(async () => {
|
||||
const label = await createLabelMutation(
|
||||
filterText,
|
||||
randomLabelColorHex(),
|
||||
''
|
||||
)
|
||||
if (label) {
|
||||
showSuccessToast(`Created label ${label.name}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
toggleLabel(label)
|
||||
} else {
|
||||
showErrorToast('Failed to create label', { position: 'bottom-right' })
|
||||
}
|
||||
}, [filterText, toggleLabel])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const maxIndex = filteredLabels.length + 1
|
||||
|
|
@ -346,24 +363,15 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}
|
||||
}
|
||||
},
|
||||
[filterText, filteredLabels, focusedIndex, isSelected, props]
|
||||
)
|
||||
|
||||
const createLabelFromFilterText = useCallback(async () => {
|
||||
const label = await createLabelMutation(
|
||||
[
|
||||
filterText,
|
||||
randomLabelColorHex(),
|
||||
''
|
||||
)
|
||||
if (label) {
|
||||
showSuccessToast(`Created label ${label.name}`, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
toggleLabel(label)
|
||||
} else {
|
||||
showErrorToast('Failed to create label', { position: 'bottom-right' })
|
||||
}
|
||||
}, [filterText, props, toggleLabel])
|
||||
filteredLabels,
|
||||
focusedIndex,
|
||||
createLabelFromFilterText,
|
||||
router,
|
||||
toggleLabel,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
|
||||
props.onOpenChange(open)
|
||||
},
|
||||
[props, selectedLabels, previousSelectedLabels, setSelectedLabels]
|
||||
[props, selectedLabels, previousSelectedLabels]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -72,7 +72,7 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
.catch((err) => {
|
||||
console.log('error saving labels: ', err)
|
||||
})
|
||||
}, [selectedLabels, setPreviousSelectedLabels])
|
||||
}, [props, selectedLabels, previousSelectedLabels, setPreviousSelectedLabels])
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={onOpenChange}>
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import { ShareModalLayout } from './ShareModal'
|
||||
import { ShareArticleView } from '../../patterns/ShareArticleView'
|
||||
|
||||
type ShareArticleModalProps = {
|
||||
url: string
|
||||
title: string
|
||||
imageURL?: string
|
||||
author?: string
|
||||
site?: string
|
||||
description?: string
|
||||
publishedAt: string
|
||||
originalArticleUrl: string
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function ShareArticleModal(
|
||||
props: ShareArticleModalProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<ShareModalLayout
|
||||
url={props.url}
|
||||
type='link'
|
||||
modalTitle='Share Article'
|
||||
title={props.title}
|
||||
description={props.description}
|
||||
onOpenChange={props.onOpenChange}
|
||||
>
|
||||
<ShareArticleView
|
||||
url={props.url}
|
||||
title={props.title}
|
||||
imageURL={props.imageURL}
|
||||
author={props.author}
|
||||
publishedAt={props.publishedAt}
|
||||
originalArticleUrl={props.originalArticleUrl}
|
||||
description={props.description}
|
||||
/>
|
||||
</ShareModalLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { HighlightView } from '../../patterns/HighlightView'
|
||||
import { ShareModalLayout } from './ShareModal'
|
||||
|
||||
type ShareHighlightModalProps = {
|
||||
highlight: Highlight
|
||||
url: string
|
||||
title: string
|
||||
author?: string
|
||||
description?: string
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function ShareHighlightModal(
|
||||
props: ShareHighlightModalProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<ShareModalLayout
|
||||
url={props.url}
|
||||
type='highlight'
|
||||
modalTitle={`Share Highlight ${props.highlight?.annotation ? '& Note' : ''}`}
|
||||
title={props.title}
|
||||
description={props.description}
|
||||
onOpenChange={props.onOpenChange}
|
||||
>
|
||||
<HighlightView
|
||||
title={props.title}
|
||||
author={props.author}
|
||||
highlight={props.highlight}
|
||||
/>
|
||||
</ShareModalLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
||||
type SkeletonArticleContainerProps = {
|
||||
|
|
@ -10,7 +9,9 @@ type SkeletonArticleContainerProps = {
|
|||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function SkeletonArticleContainer(props: SkeletonArticleContainerProps): JSX.Element {
|
||||
export function SkeletonArticleContainer(
|
||||
props: SkeletonArticleContainerProps
|
||||
): JSX.Element {
|
||||
const styles = {
|
||||
margin: props.margin ?? 360,
|
||||
fontSize: props.fontSize ?? 20,
|
||||
|
|
@ -46,14 +47,14 @@ export function SkeletonArticleContainer(props: SkeletonArticleContainerProps):
|
|||
'--blockquote-icon-font-size': '1.7rem',
|
||||
'--figure-margin': '2.6875rem auto',
|
||||
'--hr-margin': '2em',
|
||||
margin: `30px 0px`,
|
||||
margin: `0px`,
|
||||
},
|
||||
'@md': {
|
||||
maxWidth: 1024 - (styles.margin),
|
||||
maxWidth: 1024 - styles.margin,
|
||||
},
|
||||
'@lg': {
|
||||
margin: `30px 0`,
|
||||
maxWidth: 1024 - (styles.margin),
|
||||
margin: `0`,
|
||||
maxWidth: 1024 - styles.margin,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
import { ModalContent, ModalOverlay, ModalRoot } from '../../elements/ModalPrimitives'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { X, Check } from 'phosphor-react'
|
||||
import { useState } from 'react'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
|
||||
type ShareArticleModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
submit: (option: string, reminder: boolean, msg: string) => void
|
||||
}
|
||||
|
||||
enum ButtonPosition {
|
||||
Top,
|
||||
Middle,
|
||||
Bottom,
|
||||
Standalone,
|
||||
}
|
||||
|
||||
type SnoozeOptionButtonProps = {
|
||||
title: string
|
||||
position: ButtonPosition
|
||||
onClick: () => void
|
||||
selected?: boolean
|
||||
borderRadius?: string
|
||||
}
|
||||
|
||||
function SnoozeOptionButton(props: SnoozeOptionButtonProps): JSX.Element {
|
||||
let borderRadius = '0px'
|
||||
let borderWidth = '1px'
|
||||
switch (props.position) {
|
||||
case ButtonPosition.Top:
|
||||
borderWidth = '1px'
|
||||
borderRadius = '8px 8px 0px 0px'
|
||||
break
|
||||
case ButtonPosition.Middle:
|
||||
borderWidth = '0px 1px 0px 1px'
|
||||
borderRadius = '0px'
|
||||
break
|
||||
case ButtonPosition.Bottom:
|
||||
borderWidth = '1px'
|
||||
borderRadius = '0px 0px 8px 8px'
|
||||
break
|
||||
case ButtonPosition.Standalone:
|
||||
borderWidth = '1px'
|
||||
borderRadius = '8px'
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<Button style='modalOption'
|
||||
onClick={props.onClick}
|
||||
css={{
|
||||
borderRadius,
|
||||
borderWidth,
|
||||
background: props.selected ? '#FDFAEC' : undefined
|
||||
}}>
|
||||
<HStack css={{ pl: '16px', justifyContent: 'space-between', width: '100%', alignItems: 'center' }}>
|
||||
<SpanBox>{props.title}</SpanBox>
|
||||
{props.selected && (
|
||||
<Check width={24} height={24} color='#E2B513' />
|
||||
)}
|
||||
</HStack>
|
||||
</Button>)
|
||||
}
|
||||
|
||||
export function SnoozeLinkModal(
|
||||
props: ShareArticleModalProps
|
||||
): JSX.Element {
|
||||
const [sendReminder, setSendReminder] = useState(false)
|
||||
const [snoozeOption, setSnoozeOption] = useState<string | undefined>(undefined)
|
||||
|
||||
const setOption = (option: string) => {
|
||||
setSnoozeOption(option)
|
||||
setSendReminder(true)
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
css={{
|
||||
m: '0px',
|
||||
p: '0px',
|
||||
width: '375px',
|
||||
height: '388px',
|
||||
overflow: 'auto',
|
||||
background: 'white'
|
||||
}}
|
||||
>
|
||||
<VStack css={{ px: '24px', pt: '22px', m: '0px' }}>
|
||||
<HStack alignment='start' distribution='end' css={{ pb: '16px', width: '100%' }}>
|
||||
<StyledText css={{ fontSize: '24px', m: '0px', mr: '0px', color: '#0A0806' }}>Snooze</StyledText>
|
||||
<Button
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
tabIndex={-1}
|
||||
css={{ marginLeft: 'auto', p: '0px' }}
|
||||
>
|
||||
<X color='black' width={24} height={24} />
|
||||
</Button>
|
||||
</HStack>
|
||||
<VStack css={{ width: '100%', background: '#F8F8F8', borderRadius: '8px' }}>
|
||||
<SnoozeOptionButton title='Snooze until tonight' position={ButtonPosition.Top} selected={snoozeOption == 'tonight'} onClick={() => setOption('tonight')} />
|
||||
<SnoozeOptionButton title='Snooze until tomorrow' position={ButtonPosition.Middle} selected={snoozeOption == 'tomorrow'} onClick={() => setOption('tomorrow')} />
|
||||
<SnoozeOptionButton title='Snooze until the weekend' position={ButtonPosition.Bottom} selected={snoozeOption == 'weekend'} onClick={() => setOption('weekend')} />
|
||||
</VStack>
|
||||
|
||||
<Box css={{ mt: '16px', width: '100%', background: '#F8F8F8', }}>
|
||||
<SnoozeOptionButton title='Send me a reminder' position={ButtonPosition.Standalone} selected={sendReminder}
|
||||
onClick={() => {
|
||||
setSendReminder(!sendReminder)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<HStack css={{ mt: '16px', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Button
|
||||
title='Cancel'
|
||||
css={{ fontSize: '16px', width: '158px', height: '52px', background: 'unset', border: 'unset', color: '#0A0806', fontWeight: '400' }}
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>Cancel</Button>
|
||||
<Button
|
||||
style='ctaDarkYellow'
|
||||
title='Save'
|
||||
css={{ fontSize: '16px', width: '158px', height: '52px', marginLeft: 'auto', color: '#0A0806', fontWeight: '400' }}
|
||||
onClick={() => {
|
||||
if (snoozeOption) {
|
||||
let msg = 'Link snoozed until '
|
||||
switch (snoozeOption) {
|
||||
case 'tonight':
|
||||
msg += 'tonight.'
|
||||
break
|
||||
case 'tomorrow':
|
||||
msg += 'tomorrow.'
|
||||
break
|
||||
case 'weekend':
|
||||
msg += 'the weekend.'
|
||||
break
|
||||
}
|
||||
props.submit(snoozeOption, sendReminder, msg)
|
||||
props.onOpenChange(false)
|
||||
} else {
|
||||
showErrorToast('No option selected', { position: 'bottom-right' })
|
||||
}
|
||||
}}
|
||||
>Save</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import {
|
||||
ArchiveBox,
|
||||
DotsThreeOutline,
|
||||
HighlighterCircle,
|
||||
Info,
|
||||
TagSimple,
|
||||
TextAa,
|
||||
Trash,
|
||||
Tray,
|
||||
} from 'phosphor-react'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { HStack } from '../../elements/LayoutPrimitives'
|
||||
import { TooltipWrapped } from '../../elements/Tooltip'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { ReaderDropdownMenu } from '../../patterns/ReaderDropdownMenu'
|
||||
|
||||
export type ArticleActionsMenuLayout = 'top' | 'side'
|
||||
|
||||
type ArticleActionsMenuProps = {
|
||||
article?: ArticleAttributes
|
||||
layout: ArticleActionsMenuLayout
|
||||
showReaderDisplaySettings?: boolean
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
}
|
||||
|
||||
export function VerticalArticleActionsMenu(
|
||||
props: ArticleActionsMenuProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '100%',
|
||||
gap: '30px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('setLabels')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TagSimple size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('showHighlights')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="View Highlights"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<HighlighterCircle
|
||||
size={24}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('showEditModal')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Edit title & description"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<Info size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => {
|
||||
props.articleActionHandler('delete')
|
||||
}}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Delete"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<Trash size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
{!props.article?.isArchived ? (
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('archive')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Archive"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<ArchiveBox
|
||||
size={24}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('unarchive')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Unarchive"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<Tray size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => props.articleActionHandler('editDisplaySettings')}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Edit title & description"
|
||||
tooltipSide={props.layout == 'side' ? 'right' : 'bottom'}
|
||||
>
|
||||
<TextAa size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
<ReaderDropdownMenu
|
||||
triggerElement={
|
||||
<DotsThreeOutline
|
||||
size={24}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
}
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
/>
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import { useEffect, useState } from 'react'
|
|||
import { BorderedFormInput, FormLabel } from '../../elements/FormElements'
|
||||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation'
|
||||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseErrorCodes } from '../../../lib/queryParamParser'
|
||||
import { formatMessage } from '../../../locales/en/messages'
|
||||
|
|
@ -15,8 +14,9 @@ export function EmailLogin(): JSX.Element {
|
|||
const router = useRouter()
|
||||
const [email, setEmail] = useState<string | undefined>(undefined)
|
||||
const [password, setPassword] = useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { TermAndConditionsFooter } from '../LoginForm'
|
|||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
import { useValidateUsernameQuery } from '../../../lib/networking/queries/useValidateUsernameQuery'
|
||||
import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation'
|
||||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { formatMessage } from '../../../locales/en/messages'
|
||||
import { parseErrorCodes } from '../../../lib/queryParamParser'
|
||||
|
|
@ -19,10 +18,12 @@ export function EmailSignup(): JSX.Element {
|
|||
const [password, setPassword] = useState<string | undefined>(undefined)
|
||||
const [fullname, setFullname] = useState<string | undefined>(undefined)
|
||||
const [username, setUsername] = useState<string | undefined>(undefined)
|
||||
const [debouncedUsername, setDebouncedUsername] =
|
||||
useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
const [debouncedUsername, setDebouncedUsername] = useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
|
|
@ -165,7 +166,7 @@ export function EmailSignup(): JSX.Element {
|
|||
style={'ctaOutlineYellow'}
|
||||
css={{ color: '$omnivoreGray', borderColor: 'rgba(0, 0, 0, 0.06)' }}
|
||||
type="button"
|
||||
onClick={async (event) => {
|
||||
onClick={async () => {
|
||||
window.localStorage.removeItem('authVerified')
|
||||
window.localStorage.removeItem('authToken')
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Box, HStack } from '../../elements/LayoutPrimitives'
|
||||
import type { LoginFormProps } from '../LoginForm'
|
||||
|
||||
export function ResetSent(props: LoginFormProps): JSX.Element {
|
||||
export function ResetSent(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Box, HStack } from '../../elements/LayoutPrimitives'
|
||||
import type { LoginFormProps } from '../LoginForm'
|
||||
|
||||
export function VerifyEmail(props: LoginFormProps): JSX.Element {
|
||||
export function VerifyEmail(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ import {
|
|||
ModalTitleBar,
|
||||
ModalButtonBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { VStack, HStack, Box, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { VStack, Box } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { createArticleFromURLMutation } from '../../../lib/networking/mutations/createArticleFromURLMutation'
|
||||
import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation'
|
||||
import toast from 'react-hot-toast'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
|
|
@ -24,42 +20,51 @@ type AddLinkModalProps = {
|
|||
export function AddLinkModal(props: AddLinkModalProps): JSX.Element {
|
||||
const [link, setLink] = useState('')
|
||||
|
||||
const handleLinkSubmission = useCallback(async (link: string) => {
|
||||
const result = await saveUrlMutation(link)
|
||||
// const result = await saveUrlMutation(link)
|
||||
if (result && result.jobId) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
toast((t) => (
|
||||
<Box>
|
||||
Link Saved
|
||||
<span style={{ padding: '16px' }} />
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
autoFocus
|
||||
onClick={() => {
|
||||
window.location.href = `/article/sr/${result.jobId}`
|
||||
}}
|
||||
>
|
||||
Read Now
|
||||
</Button>
|
||||
</Box>
|
||||
), { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Error saving link', { position: 'bottom-right' })
|
||||
}
|
||||
}, [link])
|
||||
const handleLinkSubmission = useCallback(
|
||||
async (link: string) => {
|
||||
const result = await saveUrlMutation(link)
|
||||
// const result = await saveUrlMutation(link)
|
||||
if (result && result.jobId) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
toast(
|
||||
() => (
|
||||
<Box>
|
||||
Link Saved
|
||||
<span style={{ padding: '16px' }} />
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
autoFocus
|
||||
onClick={() => {
|
||||
window.location.href = `/article/sr/${result.jobId}`
|
||||
}}
|
||||
>
|
||||
Read Now
|
||||
</Button>
|
||||
</Box>
|
||||
),
|
||||
{ position: 'bottom-right' }
|
||||
)
|
||||
} else {
|
||||
showErrorToast('Error saving link', { position: 'bottom-right' })
|
||||
}
|
||||
},
|
||||
[link]
|
||||
)
|
||||
|
||||
const validateLink = useCallback((link: string) => {
|
||||
try {
|
||||
const url = new URL(link)
|
||||
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
||||
const validateLink = useCallback(
|
||||
(link: string) => {
|
||||
try {
|
||||
const url = new URL(link)
|
||||
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
||||
return false
|
||||
}
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, [link])
|
||||
return true
|
||||
},
|
||||
[link]
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
|
|
@ -110,7 +115,10 @@ export function AddLinkModal(props: AddLinkModalProps): JSX.Element {
|
|||
fontSize: '14px',
|
||||
}}
|
||||
/>
|
||||
<ModalButtonBar onOpenChange={props.onOpenChange} acceptButtonLabel="Add Link" />
|
||||
<ModalButtonBar
|
||||
onOpenChange={props.onOpenChange}
|
||||
acceptButtonLabel="Add Link"
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
</VStack>
|
||||
|
|
|
|||
357
packages/web/components/templates/homeFeed/EditItemModals.tsx
Normal file
357
packages/web/components/templates/homeFeed/EditItemModals.tsx
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
import {
|
||||
ModalRoot,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { VStack, HStack, Box, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { StyledTextArea } from '../../elements/StyledTextArea'
|
||||
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { CloseButton } from '../../elements/CloseButton'
|
||||
|
||||
type EditLibraryItemModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
item: LibraryItem
|
||||
updateItem: (item: LibraryItem) => Promise<void>
|
||||
}
|
||||
|
||||
export function EditLibraryItemModal(
|
||||
props: EditLibraryItemModalProps
|
||||
): JSX.Element {
|
||||
const onSave = useCallback(
|
||||
(
|
||||
title: string,
|
||||
author: string | undefined,
|
||||
description: string,
|
||||
savedAt: Dayjs,
|
||||
publishedAt: Dayjs | undefined
|
||||
) => {
|
||||
;(async () => {
|
||||
if (title !== '') {
|
||||
const res = await updatePageMutation({
|
||||
pageId: props.item.node.id,
|
||||
title,
|
||||
description,
|
||||
byline: author,
|
||||
savedAt: savedAt.toISOString(),
|
||||
publishedAt: publishedAt ? publishedAt.toISOString() : undefined,
|
||||
})
|
||||
|
||||
if (res) {
|
||||
await props.updateItem({
|
||||
cursor: props.item.cursor,
|
||||
node: {
|
||||
...props.item.node,
|
||||
title: title,
|
||||
author: author,
|
||||
description: description,
|
||||
},
|
||||
})
|
||||
showSuccessToast('Link updated succesfully', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
props.onOpenChange(false)
|
||||
} else {
|
||||
showErrorToast('There was an error updating your link', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
showErrorToast('Title must be a non-empty value', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
})()
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
||||
return (
|
||||
<EditItemModal
|
||||
title={props.item.node.title}
|
||||
author={props.item.node.author}
|
||||
description={props.item.node.description}
|
||||
savedAt={dayjs(props.item.node.savedAt)}
|
||||
publishedAt={
|
||||
props.item.node.publishedAt
|
||||
? dayjs(props.item.node.publishedAt)
|
||||
: undefined
|
||||
}
|
||||
onOpenChange={props.onOpenChange}
|
||||
onSave={onSave}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type EditArticleModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
article: ArticleAttributes
|
||||
updateArticle: (
|
||||
title: string,
|
||||
author: string | undefined,
|
||||
description: string,
|
||||
savedAt: string,
|
||||
publishedAt: string | undefined
|
||||
) => void
|
||||
}
|
||||
|
||||
export function EditArticleModal(props: EditArticleModalProps): JSX.Element {
|
||||
const onSave = useCallback(
|
||||
(
|
||||
title: string,
|
||||
author: string | undefined,
|
||||
description: string,
|
||||
savedAt: Dayjs,
|
||||
publishedAt: Dayjs | undefined
|
||||
) => {
|
||||
;(async () => {
|
||||
if (title !== '') {
|
||||
const res = await updatePageMutation({
|
||||
pageId: props.article.id,
|
||||
title,
|
||||
description,
|
||||
byline: author,
|
||||
savedAt: savedAt.toISOString(),
|
||||
publishedAt: publishedAt ? publishedAt.toISOString() : undefined,
|
||||
})
|
||||
if (res) {
|
||||
props.updateArticle(
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
savedAt.toISOString(),
|
||||
publishedAt ? publishedAt.toISOString() : undefined
|
||||
)
|
||||
showSuccessToast('Link updated succesfully', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
props.onOpenChange(false)
|
||||
} else {
|
||||
showErrorToast('There was an error updating your link', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
showErrorToast('Title must be a non-empty value', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
})()
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
||||
return (
|
||||
<EditItemModal
|
||||
title={props.article.title}
|
||||
author={props.article.author}
|
||||
description={props.article.description || ''}
|
||||
savedAt={dayjs(props.article.savedAt)}
|
||||
publishedAt={
|
||||
props.article.publishedAt ? dayjs(props.article.publishedAt) : undefined
|
||||
}
|
||||
onOpenChange={props.onOpenChange}
|
||||
onSave={onSave}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type EditItemModalProps = {
|
||||
title: string
|
||||
author: string | undefined
|
||||
description: string
|
||||
|
||||
savedAt: Dayjs
|
||||
publishedAt: Dayjs | undefined
|
||||
onOpenChange: (open: boolean) => void
|
||||
|
||||
onSave: (
|
||||
title: string,
|
||||
author: string | undefined,
|
||||
description: string,
|
||||
savedAt: Dayjs,
|
||||
publishedAt: Dayjs | undefined
|
||||
) => void
|
||||
}
|
||||
|
||||
function EditItemModal(props: EditItemModalProps): JSX.Element {
|
||||
const [title, setTitle] = useState(props.title)
|
||||
const [author, setAuthor] = useState(props.author)
|
||||
const [savedAt, setSavedAt] = useState(props.savedAt)
|
||||
const [publishedAt, setPublishedAt] = useState(props.publishedAt)
|
||||
const [description, setDescription] = useState(props.description)
|
||||
|
||||
const titleStyle = {
|
||||
mt: '22px',
|
||||
mb: '2px',
|
||||
fontFamily: '$display',
|
||||
fontWeight: '600',
|
||||
fontSize: '11px',
|
||||
color: '#898989',
|
||||
}
|
||||
|
||||
const inputStyle = {
|
||||
mt: '1px',
|
||||
borderRadius: '5px',
|
||||
border: '1px solid $thBorderColor',
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: '500',
|
||||
fontSize: '16px',
|
||||
height: '38px',
|
||||
p: '5px',
|
||||
color: '$thTextContrast2',
|
||||
'&:focus': {
|
||||
outline: 'none !important',
|
||||
border: '1px solid $omnivoreCtaYellow',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange} css={{}}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
css={{ bg: '$grayBg', p: '20px', maxWidth: '420px' }}
|
||||
onInteractOutside={() => {
|
||||
// remove focus from modal
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
>
|
||||
<VStack distribution="start" css={{ p: '0px' }}>
|
||||
<Header onOpenChange={props.onOpenChange} />
|
||||
<Box css={{ width: '100%' }}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<HStack distribution="start" css={{ width: '100%' }}>
|
||||
<VStack css={{ width: '45%' }}>
|
||||
<StyledText css={titleStyle}>SAVED AT:</StyledText>
|
||||
<FormInput
|
||||
type="datetime-local"
|
||||
value={props.savedAt.format('YYYY-MM-DDThh:mm')}
|
||||
placeholder="Edit Date"
|
||||
onChange={(event) => {
|
||||
const dateStr = event.target.value
|
||||
setSavedAt(dayjs(dateStr))
|
||||
}}
|
||||
css={{
|
||||
...inputStyle,
|
||||
fontSize: '14px',
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
<VStack css={{ width: '45%', marginLeft: 'auto' }}>
|
||||
<StyledText css={titleStyle}>PUBLISHED AT</StyledText>
|
||||
<FormInput
|
||||
type="datetime-local"
|
||||
value={
|
||||
props.publishedAt
|
||||
? props.publishedAt.format('YYYY-MM-DDThh:mm')
|
||||
: undefined
|
||||
}
|
||||
placeholder="Edit Published Date"
|
||||
onChange={(event) => {
|
||||
const dateStr = event.target.value
|
||||
setPublishedAt(dayjs(dateStr))
|
||||
}}
|
||||
css={{
|
||||
...inputStyle,
|
||||
fontSize: '14px',
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<StyledText css={titleStyle}>TITLE</StyledText>
|
||||
<FormInput
|
||||
type="text"
|
||||
value={title}
|
||||
autoFocus
|
||||
placeholder="Edit Title"
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
onFocus={(event) => {
|
||||
event.target.select()
|
||||
}}
|
||||
css={inputStyle}
|
||||
/>
|
||||
<StyledText css={titleStyle}>AUTHOR</StyledText>
|
||||
<FormInput
|
||||
type="author"
|
||||
value={author}
|
||||
placeholder="Edit Author"
|
||||
onChange={(event) => setAuthor(event.target.value)}
|
||||
onFocus={(event) => {
|
||||
event.target.select()
|
||||
}}
|
||||
css={inputStyle}
|
||||
/>
|
||||
<StyledText css={titleStyle}>DESCRIPTION</StyledText>
|
||||
<StyledTextArea
|
||||
css={{
|
||||
...inputStyle,
|
||||
mt: '2px',
|
||||
width: '100%',
|
||||
height: '120px',
|
||||
}}
|
||||
placeholder="Edit Description"
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
onFocus={(event) => {
|
||||
event.target.select()
|
||||
}}
|
||||
maxLength={4000}
|
||||
/>
|
||||
<HStack distribution="end" css={{ mt: '12px', width: '100%' }}>
|
||||
<Button
|
||||
onClick={() => props.onOpenChange(false)}
|
||||
style="cancelGeneric"
|
||||
css={{ mr: '5px' }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
props.onSave(
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
savedAt,
|
||||
publishedAt
|
||||
)
|
||||
}}
|
||||
style="ctaDarkYellow"
|
||||
css={{ mb: '0px' }}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</HStack>
|
||||
</form>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
function Header(props: HeaderProps): JSX.Element {
|
||||
return (
|
||||
<HStack distribution="start" alignment="center" css={{ width: '100%' }}>
|
||||
<StyledText style="modalHeadline">Edit Title & Description</StyledText>
|
||||
<SpanBox css={{ marginLeft: 'auto' }}>
|
||||
<CloseButton close={() => props.onOpenChange(false)} />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
import {
|
||||
ModalRoot,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { VStack, HStack, Box } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { useState } from 'react'
|
||||
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { StyledTextArea } from '../../elements/StyledTextArea'
|
||||
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
|
||||
type EditTitleModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
item: LibraryItem
|
||||
updateItem: (item: LibraryItem) => Promise<void>
|
||||
}
|
||||
|
||||
export function EditTitleModal(props: EditTitleModalProps): JSX.Element {
|
||||
const [title, setTitle] = useState(props.item.node.title)
|
||||
const [author, setAuthor] = useState(props.item.node.author)
|
||||
const [description, setDescription] = useState(props.item.node.description)
|
||||
|
||||
const handleUpdateTitle = async () => {
|
||||
if (title !== '') {
|
||||
const res = await updatePageMutation({
|
||||
pageId: props.item.node.id,
|
||||
title,
|
||||
description,
|
||||
byline: author,
|
||||
})
|
||||
|
||||
if (res) {
|
||||
await props.updateItem({
|
||||
cursor: props.item.cursor,
|
||||
node: {
|
||||
...props.item.node,
|
||||
title: title,
|
||||
author: author,
|
||||
description: description,
|
||||
},
|
||||
})
|
||||
showSuccessToast('Link updated succesfully', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
props.onOpenChange(false)
|
||||
} else {
|
||||
showErrorToast('There was an error updating your link', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
showErrorToast('Title must be a non-empty value', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
css={{ bg: '$grayBg', pt: '0px' }}
|
||||
onInteractOutside={() => {
|
||||
// remove focus from modal
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
>
|
||||
<VStack distribution="start" css={{ p: '$2' }}>
|
||||
<HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{ width: '100%', mt: '4px' }}
|
||||
>
|
||||
<StyledText style="modalHeadline">
|
||||
Edit Title and Description
|
||||
</StyledText>
|
||||
<Button
|
||||
css={{ p: '10px', cursor: 'pointer', pt: '2px' }}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<CrossIcon
|
||||
size={11}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
<StyledText css={{ mt: '22px', mb: '6px' }}>Title</StyledText>
|
||||
<Box css={{ width: '100%' }}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
type="text"
|
||||
value={title}
|
||||
autoFocus
|
||||
placeholder="Edit Title"
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
css={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid $grayTextContrast',
|
||||
width: '100%',
|
||||
p: '$2',
|
||||
}}
|
||||
/>
|
||||
<StyledText css={{ mt: '22px', mb: '6px' }}>Author</StyledText>
|
||||
<FormInput
|
||||
type="author"
|
||||
value={author}
|
||||
autoFocus
|
||||
placeholder="Edit Author"
|
||||
onChange={(event) => setAuthor(event.target.value)}
|
||||
css={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid $grayTextContrast',
|
||||
width: '100%',
|
||||
p: '$2',
|
||||
}}
|
||||
/>
|
||||
<StyledText css={{ mt: '22px', mb: '6px' }}>
|
||||
Description
|
||||
</StyledText>
|
||||
<Box
|
||||
css={{
|
||||
border: '1px solid $grayTextContrast',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<StyledTextArea
|
||||
css={{
|
||||
p: '14px',
|
||||
height: '$6',
|
||||
width: '100%',
|
||||
}}
|
||||
placeholder="Edit Description"
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
maxLength={4000}
|
||||
/>
|
||||
</Box>
|
||||
<HStack distribution="end" css={{ mt: '12px', width: '100%' }}>
|
||||
<Button
|
||||
onClick={() => props.onOpenChange(false)}
|
||||
style="ctaOutlineYellow"
|
||||
css={{ mr: '16px' }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateTitle}
|
||||
style="ctaDarkYellow"
|
||||
css={{ mb: '0px' }}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</form>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,15 +1,8 @@
|
|||
|
||||
|
||||
// There aren't any discussions.
|
||||
// You can open a new discussion to ask questions about this repository or get help.
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Book } from 'phosphor-react'
|
||||
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Box, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { VStack } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
||||
type EmptyLibraryProps = {
|
||||
|
|
@ -18,19 +11,33 @@ type EmptyLibraryProps = {
|
|||
|
||||
export function EmptyLibrary(props: EmptyLibraryProps): JSX.Element {
|
||||
return (
|
||||
<VStack alignment="center" distribution="center" css={{ color: '$grayTextContrast', textAlign: 'center', paddingTop: '88px' }}>
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
textAlign: 'center',
|
||||
paddingTop: '88px',
|
||||
}}
|
||||
>
|
||||
<Book size={44} color={theme.colors.grayTextContrast.toString()} />
|
||||
<StyledText style="fixedHeadline" css={{ color: '$grayTextContrast' }}>
|
||||
No results found.
|
||||
</StyledText>
|
||||
<StyledText style="fixedHeadline" css={{ color: '$grayTextContrast' }}>
|
||||
No results found.
|
||||
</StyledText>
|
||||
|
||||
<StyledText style="footnote" css={{ color: '$grayTextContrast' }}>
|
||||
You can add a link or read more about Omnivore's <Link href="/help/search">advanced search</Link>.
|
||||
</StyledText>
|
||||
|
||||
<Button style="ctaDarkYellow" onClick={() => { props.onAddLinkClicked() }}>
|
||||
Add Link
|
||||
</Button>
|
||||
<StyledText style="footnote" css={{ color: '$grayTextContrast' }}>
|
||||
You can add a link or read more about Omnivore's{' '}
|
||||
<Link href="/help/search">advanced search</Link>.
|
||||
</StyledText>
|
||||
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={() => {
|
||||
props.onAddLinkClicked()
|
||||
}}
|
||||
>
|
||||
Add Link
|
||||
</Button>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
18
packages/web/components/templates/homeFeed/HeaderSpacer.tsx
Normal file
18
packages/web/components/templates/homeFeed/HeaderSpacer.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
|
||||
export const HEADER_HEIGHT = '105px'
|
||||
export const MOBILE_HEADER_HEIGHT = '70px'
|
||||
|
||||
export function HeaderSpacer(): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
height: HEADER_HEIGHT,
|
||||
bg: '$grayBase',
|
||||
'@mdDown': {
|
||||
height: MOBILE_HEADER_HEIGHT,
|
||||
},
|
||||
}}
|
||||
></Box>
|
||||
)
|
||||
}
|
||||
275
packages/web/components/templates/homeFeed/HighlightItem.tsx
Normal file
275
packages/web/components/templates/homeFeed/HighlightItem.tsx
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DotsThreeVertical } from 'phosphor-react'
|
||||
import { Fragment, useCallback, useMemo, useState } from 'react'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight'
|
||||
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { HighlightNoteTextEditArea } from '../../elements/HighlightNoteTextEditArea'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import {
|
||||
Blockquote,
|
||||
Box,
|
||||
HStack,
|
||||
SpanBox,
|
||||
VStack,
|
||||
} from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
|
||||
import { SetLabelsModal } from '../article/SetLabelsModal'
|
||||
|
||||
type HighlightItemProps = {
|
||||
highlight: Highlight
|
||||
viewer: UserBasicData | undefined
|
||||
item: LibraryItemNode
|
||||
}
|
||||
|
||||
const StyledQuote = styled(Blockquote, {
|
||||
margin: '0px',
|
||||
fontSize: '16px',
|
||||
fontFamily: '$inter',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.50',
|
||||
color: '$thHighContrast',
|
||||
paddingLeft: '15px',
|
||||
borderLeft: '2px solid $omnivoreCtaYellow',
|
||||
})
|
||||
|
||||
export function HighlightItem(props: HighlightItemProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [hover, setHover] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
|
||||
const lines = useMemo(
|
||||
() => props.highlight.quote.split('\n'),
|
||||
[props.highlight.quote]
|
||||
)
|
||||
|
||||
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
|
||||
useState<undefined | string>(undefined)
|
||||
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [, updateState] = useState({})
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
css={{ width: '100%', py: '20px', cursor: 'pointer' }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'clip',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<StyledQuote
|
||||
onClick={(event) => {
|
||||
if (router && props.viewer) {
|
||||
const dest = `/${props.viewer}/${props.item.slug}#${props.highlight.id}`
|
||||
router.push(dest)
|
||||
}
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<SpanBox css={{ p: '1px', borderRadius: '2px' }}>
|
||||
{lines.map((line: string, index: number) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
{index !== lines.length - 1 && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</SpanBox>
|
||||
</StyledQuote>
|
||||
|
||||
<Box css={{ display: 'block' }}>
|
||||
{props.highlight.labels?.map((label: Label, index: number) => (
|
||||
<LabelChip
|
||||
key={index}
|
||||
text={label.name || ''}
|
||||
color={label.color}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{!isEditing && (
|
||||
<StyledText
|
||||
css={{
|
||||
borderRadius: '6px',
|
||||
bg: '#EBEBEB',
|
||||
p: '10px',
|
||||
width: '100%',
|
||||
marginTop: '5px',
|
||||
color: '#3D3D3D',
|
||||
}}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{props.highlight.annotation
|
||||
? props.highlight.annotation
|
||||
: 'Add your notes...'}
|
||||
</StyledText>
|
||||
)}
|
||||
{isEditing && (
|
||||
<HighlightNoteTextEditArea
|
||||
setIsEditing={setIsEditing}
|
||||
highlight={props.highlight}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
updateHighlight={() => {}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
<SpanBox
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
width: '20px',
|
||||
visibility: hover ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HighlightsMenu
|
||||
highlight={props.highlight}
|
||||
setLabelsTarget={setLabelsTarget}
|
||||
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
|
||||
/>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
{showConfirmDeleteHighlightId && (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure you want to delete this highlight?'}
|
||||
onAccept={async () => {
|
||||
setShowConfirmDeleteHighlightId(undefined)
|
||||
const result = await deleteHighlightMutation(
|
||||
showConfirmDeleteHighlightId
|
||||
)
|
||||
if (result) {
|
||||
showSuccessToast('Highlight deleted')
|
||||
} else {
|
||||
showErrorToast('Error deleting highlight')
|
||||
}
|
||||
}}
|
||||
onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)}
|
||||
/>
|
||||
)}
|
||||
{labelsTarget && (
|
||||
<SetLabelsModal
|
||||
provider={labelsTarget}
|
||||
onOpenChange={function (open: boolean): void {
|
||||
setLabelsTarget(undefined)
|
||||
}}
|
||||
onLabelsUpdated={function (labels: Label[]): void {
|
||||
updateState({})
|
||||
}}
|
||||
save={function (labels: Label[]): Promise<Label[] | undefined> {
|
||||
const result = setLabelsForHighlight(
|
||||
labelsTarget.id,
|
||||
labels.map((label) => label.id)
|
||||
)
|
||||
return result
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightsMenuProps = {
|
||||
highlight: Highlight
|
||||
|
||||
setLabelsTarget: (target: Highlight) => void
|
||||
setShowConfirmDeleteHighlightId: (set: string) => void
|
||||
}
|
||||
|
||||
function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
|
||||
const copyHighlight = useCallback(() => {
|
||||
;(async () => {
|
||||
await navigator.clipboard.writeText(props.highlight.quote)
|
||||
showSuccessToast('Highlight copied')
|
||||
})()
|
||||
}, [props.highlight])
|
||||
|
||||
const exportHighlight = useCallback(() => {
|
||||
;(async () => {
|
||||
const markdown = highlightAsMarkdown(props.highlight)
|
||||
await navigator.clipboard.writeText(markdown)
|
||||
showSuccessToast('Highlight copied')
|
||||
})()
|
||||
}, [props.highlight])
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
triggerElement={
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
'&:hover': {
|
||||
bg: '#898989',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DotsThreeVertical size={20} color="#EBEBEB" weight="bold" />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={async () => {
|
||||
copyHighlight()
|
||||
}}
|
||||
title="Copy"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
props.setLabelsTarget(props.highlight)
|
||||
}}
|
||||
title="Labels"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
exportHighlight()
|
||||
}}
|
||||
title="Delete"
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export function highlightAsMarkdown(highlight: Highlight) {
|
||||
let buffer = `> ${highlight.quote}`
|
||||
if (highlight.annotation) {
|
||||
buffer += `\n\n${highlight.annotation}`
|
||||
}
|
||||
buffer += '\n'
|
||||
return buffer
|
||||
}
|
||||
|
||||
export function highlightsAsMarkdown(highlights: Highlight[]) {
|
||||
return highlights
|
||||
.map((highlight) => {
|
||||
return highlightAsMarkdown(highlight)
|
||||
})
|
||||
.join('\n\n')
|
||||
}
|
||||
369
packages/web/components/templates/homeFeed/HighlightsLayout.tsx
Normal file
369
packages/web/components/templates/homeFeed/HighlightsLayout.tsx
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import { DotsThreeVertical, HighlighterCircle } from 'phosphor-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import {
|
||||
MetaStyle,
|
||||
timeAgo,
|
||||
} from '../../patterns/LibraryCards/LibraryCardStyles'
|
||||
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
|
||||
import { HighlightItem, highlightsAsMarkdown } from './HighlightItem'
|
||||
|
||||
type HighlightItemsLayoutProps = {
|
||||
items: LibraryItem[]
|
||||
viewer: UserBasicData | undefined
|
||||
|
||||
gridContainerRef: React.RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
export function HighlightItemsLayout(
|
||||
props: HighlightItemsLayoutProps
|
||||
): JSX.Element {
|
||||
const [currentItem, setCurrentItem] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Only set the current item on larger screens
|
||||
if (window.innerWidth >= 992 /* lgDown */) {
|
||||
if (!currentItem && props.items.length > 0) {
|
||||
setCurrentItem(props.items[0])
|
||||
}
|
||||
}
|
||||
}, [currentItem, setCurrentItem, props.items])
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bg: '$thBackground2',
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
<Toaster />
|
||||
<VStack
|
||||
css={{
|
||||
width: '430px',
|
||||
height: '100%',
|
||||
bg: '$thBackground',
|
||||
'@lgDown': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: 'calc(100% - 35px)',
|
||||
height: '55px',
|
||||
mx: '20px',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
>
|
||||
{/* <Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
marginLeft: 'auto',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
'&:hover': {
|
||||
bg: '#898989',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DotsThreeVertical size={200} color="#898989" weight="bold" />
|
||||
</Box> */}
|
||||
</HStack>
|
||||
{props.items.map((linkedItem) => (
|
||||
<Box
|
||||
className="linkedItemCard"
|
||||
data-testid="linkedItemCard"
|
||||
id={linkedItem.node.id}
|
||||
tabIndex={0}
|
||||
key={linkedItem.node.id}
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
px: '15px',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
setCurrentItem(linkedItem)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.viewer && (
|
||||
<LibraryItemCard
|
||||
item={linkedItem}
|
||||
viewer={props.viewer}
|
||||
selected={currentItem?.node.id == linkedItem.node.id}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
{currentItem && (
|
||||
<>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
flexGrow: '1',
|
||||
'@lgDown': {
|
||||
display: 'none',
|
||||
flexGrow: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HighlightList item={currentItem} viewer={props.viewer} />
|
||||
</SpanBox>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightTitleCardProps = {
|
||||
item: LibraryItem
|
||||
viewer: UserBasicData
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
function LibraryItemCard(props: HighlightTitleCardProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<SpanBox css={{ display: 'none', '@lgDown': { display: 'flex' } }}>
|
||||
<LibraryHighlightGridCard
|
||||
item={props.item.node}
|
||||
viewer={props.viewer}
|
||||
/>
|
||||
</SpanBox>
|
||||
<SpanBox css={{ display: 'none', '@lg': { display: 'flex' } }}>
|
||||
<HighlightTitleCard {...props} />
|
||||
</SpanBox>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function HighlightTitleCard(props: HighlightTitleCardProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
py: '10px',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
}}
|
||||
distribution="start"
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
py: '15px',
|
||||
px: '15px',
|
||||
borderRadius: '10px',
|
||||
bg: props.selected ? '$thBackground2' : 'unset',
|
||||
}}
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<HStack css={MetaStyle} distribution="start">
|
||||
<Box>
|
||||
{timeAgo(props.item.node.savedAt)}
|
||||
{` `}
|
||||
{props.item.node.wordsCount ?? 0 > 0
|
||||
? ` • ${Math.max(
|
||||
1,
|
||||
Math.round((props.item.node.wordsCount ?? 0) / 235)
|
||||
)} min read`
|
||||
: null}
|
||||
{props.item.node.readingProgressPercent ?? 0 > 0 ? (
|
||||
<>
|
||||
{` • `}
|
||||
<SpanBox css={{ color: '#55B938' }}>
|
||||
{`${Math.round(props.item.node.readingProgressPercent)}%`}
|
||||
</SpanBox>
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box
|
||||
css={{
|
||||
mt: '5px',
|
||||
color: '$thTextContrast2',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.50',
|
||||
fontFamily: '$display',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{props.item.node.title}
|
||||
</Box>
|
||||
</VStack>
|
||||
|
||||
<HighlightCountChip
|
||||
selected={props.selected}
|
||||
count={props.item.node.highlights?.length ?? 0}
|
||||
/>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightListProps = {
|
||||
item: LibraryItem
|
||||
viewer: UserBasicData | undefined
|
||||
}
|
||||
|
||||
function HighlightList(props: HighlightListProps): JSX.Element {
|
||||
const exportHighlights = useCallback(() => {
|
||||
;(async () => {
|
||||
if (!props.item.node.highlights) {
|
||||
showErrorToast('No highlights to export')
|
||||
return
|
||||
}
|
||||
const markdown = highlightsAsMarkdown(props.item.node.highlights)
|
||||
await navigator.clipboard.writeText(markdown)
|
||||
showSuccessToast('Highlight copied')
|
||||
})()
|
||||
}, [props.item.node.highlights])
|
||||
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
m: '20px',
|
||||
height: '100%',
|
||||
flexGrow: '1',
|
||||
}}
|
||||
distribution="center"
|
||||
alignment="start"
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
width: '425px',
|
||||
height: '100%',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pt: '25px',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
fontFamily: '$display',
|
||||
width: '100%',
|
||||
color: '$thTextContrast2',
|
||||
}}
|
||||
>
|
||||
HIGHLIGHTS
|
||||
</StyledText>
|
||||
<Dropdown
|
||||
triggerElement={
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
'&:hover': {
|
||||
bg: '#898989',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DotsThreeVertical size={20} color="#EBEBEB" weight="bold" />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
exportHighlights()
|
||||
}}
|
||||
title="Export"
|
||||
/>
|
||||
</Dropdown>
|
||||
</HStack>
|
||||
<VStack css={{ height: '100%', width: '100%' }} distribution="start">
|
||||
{(props.item.node.highlights ?? []).map((highlight) => (
|
||||
<HighlightItem
|
||||
key={highlight.id}
|
||||
viewer={props.viewer}
|
||||
item={props.item.node}
|
||||
highlight={highlight}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightCountChipProps = {
|
||||
count: number
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
function HighlightCountChip(props: HighlightCountChipProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
gap: '5px',
|
||||
borderRadius: '12px',
|
||||
marginLeft: 'auto',
|
||||
minWidth: '50px',
|
||||
minHeight: '25px',
|
||||
width: '47px',
|
||||
color: props.selected ? '#3D3D3D' : '#898989',
|
||||
bg: props.selected ? '#D9D9D9' : '$thBackground2',
|
||||
fontFamily: '$inter',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
ml: '30px',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
>
|
||||
{props.count}
|
||||
<HighlighterCircle
|
||||
size={15}
|
||||
color={props.selected ? '#3D3D3D' : '#898989'}
|
||||
/>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -6,34 +6,23 @@ import type {
|
|||
LibraryItemsQueryInput,
|
||||
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import {
|
||||
useGetViewerQuery,
|
||||
UserBasicData,
|
||||
} from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes'
|
||||
import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { LibrarySearchBar } from './LibrarySearchBar'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { AddLinkModal } from './AddLinkModal'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { ListLayoutIcon } from '../../elements/images/ListLayoutIcon'
|
||||
import { GridLayoutIcon } from '../../elements/images/GridLayoutIcon'
|
||||
import {
|
||||
libraryListCommands,
|
||||
searchBarCommands,
|
||||
} from '../../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { libraryListCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { ShareArticleModal } from '../article/ShareArticleModal'
|
||||
import { webBaseURL } from '../../../lib/appConfig'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { SnoozeLinkModal } from '../article/SnoozeLinkModal'
|
||||
import {
|
||||
createReminderMutation,
|
||||
ReminderType,
|
||||
} from '../../../lib/networking/mutations/createReminderMutation'
|
||||
import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
|
||||
import { SetLabelsModal } from '../article/SetLabelsModal'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
|
|
@ -44,7 +33,7 @@ import {
|
|||
State,
|
||||
} from '../../../lib/networking/fragments/articleFragment'
|
||||
import { Action, createAction, useKBar, useRegisterActions } from 'kbar'
|
||||
import { EditTitleModal } from './EditTitleModal'
|
||||
import { EditLibraryItemModal } from './EditItemModals'
|
||||
import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
|
||||
import debounce from 'lodash/debounce'
|
||||
import {
|
||||
|
|
@ -55,22 +44,12 @@ import {
|
|||
import axios from 'axios'
|
||||
import { uploadFileRequestMutation } from '../../../lib/networking/mutations/uploadFileMutation'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
import { LibraryHeader } from './LibraryHeader'
|
||||
import { LibraryFilterMenu } from './LibraryFilterMenu'
|
||||
import { HighlightItemsLayout } from './HighlightsLayout'
|
||||
|
||||
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
|
||||
|
||||
const timeZoneHourDiff = -new Date().getTimezoneOffset() / 60
|
||||
|
||||
const SAVED_SEARCHES: Record<string, string> = {
|
||||
Inbox: `in:inbox`,
|
||||
'Read Later': `in:inbox -label:Newsletter`,
|
||||
Highlights: `type:highlights`,
|
||||
Today: `in:inbox saved:${
|
||||
new Date(new Date().getTime() - 24 * 3600000).toISOString().split('T')[0]
|
||||
}Z${timeZoneHourDiff.toLocaleString('en-US', {
|
||||
signDisplay: 'always',
|
||||
})}..*`,
|
||||
Newsletters: `in:inbox label:Newsletter`,
|
||||
}
|
||||
export type LibraryMode = 'reads' | 'highlights'
|
||||
|
||||
const fetchSearchResults = async (query: string, cb: any) => {
|
||||
if (!query.startsWith('#')) return
|
||||
|
|
@ -92,6 +71,7 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
const router = useRouter()
|
||||
const { queryValue } = useKBar((state) => ({ queryValue: state.searchQuery }))
|
||||
const [searchResults, setSearchResults] = useState<SearchItem[]>([])
|
||||
const [mode, setMode] = useState<LibraryMode>('reads')
|
||||
|
||||
const defaultQuery = {
|
||||
limit: 10,
|
||||
|
|
@ -101,14 +81,6 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
|
||||
const gridContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [shareTarget, setShareTarget] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [snoozeTarget, setSnoozeTarget] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
|
@ -122,14 +94,6 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
const [queryInputs, setQueryInputs] =
|
||||
useState<LibraryItemsQueryInput>(defaultQuery)
|
||||
|
||||
useKeyboardShortcuts(
|
||||
searchBarCommands((action) => {
|
||||
if (action === 'clearSearch') {
|
||||
setQueryInputs(defaultQuery)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const {
|
||||
itemsPages,
|
||||
size,
|
||||
|
|
@ -150,6 +114,17 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
} else setSearchResults([])
|
||||
}, [queryValue])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
queryInputs.searchQuery &&
|
||||
queryInputs.searchQuery?.indexOf('mode:highlights') > -1
|
||||
) {
|
||||
setMode('highlights')
|
||||
} else {
|
||||
setMode('reads')
|
||||
}
|
||||
}, [queryInputs])
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
const q = router.query['q']
|
||||
|
|
@ -161,9 +136,17 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
setQueryInputs({ ...queryInputs, searchQuery: qs })
|
||||
performActionOnItem('refresh', undefined as unknown as any)
|
||||
}
|
||||
const mode = router.query['mode']
|
||||
|
||||
// intentionally not watching queryInputs here to prevent infinite looping
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setQueryInputs, router.isReady, router.query, performActionOnItem])
|
||||
}, [
|
||||
setMode,
|
||||
setQueryInputs,
|
||||
router.isReady,
|
||||
router.query,
|
||||
performActionOnItem,
|
||||
])
|
||||
|
||||
const hasMore = useMemo(() => {
|
||||
if (!itemsPages) {
|
||||
|
|
@ -339,12 +322,6 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
case 'mark-unread':
|
||||
performActionOnItem('mark-unread', item)
|
||||
break
|
||||
case 'share':
|
||||
setShareTarget(item)
|
||||
break
|
||||
case 'snooze':
|
||||
setSnoozeTarget(item)
|
||||
break
|
||||
case 'set-labels':
|
||||
setLabelsTarget(item)
|
||||
break
|
||||
|
|
@ -357,22 +334,8 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
}
|
||||
|
||||
const modalTargetItem = useMemo(() => {
|
||||
return (
|
||||
labelsTarget ||
|
||||
snoozeTarget ||
|
||||
shareTarget ||
|
||||
linkToEdit ||
|
||||
linkToRemove ||
|
||||
linkToUnsubscribe
|
||||
)
|
||||
}, [
|
||||
labelsTarget,
|
||||
snoozeTarget,
|
||||
shareTarget,
|
||||
linkToEdit,
|
||||
linkToRemove,
|
||||
linkToUnsubscribe,
|
||||
])
|
||||
return labelsTarget || linkToEdit || linkToRemove || linkToUnsubscribe
|
||||
}, [labelsTarget, linkToEdit, linkToRemove, linkToUnsubscribe])
|
||||
|
||||
useKeyboardShortcuts(
|
||||
libraryListCommands((action) => {
|
||||
|
|
@ -472,9 +435,6 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
case 'showEditLabelsModal':
|
||||
handleCardAction('set-labels', activeItem)
|
||||
break
|
||||
case 'shareItem':
|
||||
setShareTarget(activeItem)
|
||||
break
|
||||
case 'sortDescending':
|
||||
setQueryInputs({ ...queryInputs, sortDescending: true })
|
||||
break
|
||||
|
|
@ -572,6 +532,8 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
reloadItems={mutate}
|
||||
searchTerm={queryInputs.searchQuery}
|
||||
gridContainerRef={gridContainerRef}
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
setQueryInputs({
|
||||
...queryInputs,
|
||||
|
|
@ -583,8 +545,10 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
} else {
|
||||
qp.delete('q')
|
||||
}
|
||||
|
||||
const href = `${window.location.pathname}?${qp.toString()}`
|
||||
router.push(href, href, { shallow: true })
|
||||
window.sessionStorage.setItem('q', qp.toString())
|
||||
performActionOnItem('refresh', undefined as unknown as any)
|
||||
}}
|
||||
loadMore={() => {
|
||||
|
|
@ -597,10 +561,6 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
hasData={!!itemsPages}
|
||||
totalItems={itemsPages?.[0].search.pageInfo.totalCount || 0}
|
||||
isValidating={isValidating}
|
||||
shareTarget={shareTarget}
|
||||
setShareTarget={setShareTarget}
|
||||
snoozeTarget={snoozeTarget}
|
||||
setSnoozeTarget={setSnoozeTarget}
|
||||
labelsTarget={labelsTarget}
|
||||
setLabelsTarget={setLabelsTarget}
|
||||
showAddLinkModal={showAddLinkModal}
|
||||
|
|
@ -631,10 +591,6 @@ type HomeFeedContentProps = {
|
|||
totalItems: number
|
||||
isValidating: boolean
|
||||
loadMore: () => void
|
||||
shareTarget: LibraryItem | undefined
|
||||
setShareTarget: (target: LibraryItem | undefined) => void
|
||||
snoozeTarget: LibraryItem | undefined
|
||||
setSnoozeTarget: (target: LibraryItem | undefined) => void
|
||||
labelsTarget: LibraryItem | undefined
|
||||
setLabelsTarget: (target: LibraryItem | undefined) => void
|
||||
showAddLinkModal: boolean
|
||||
|
|
@ -650,22 +606,48 @@ type HomeFeedContentProps = {
|
|||
linkToUnsubscribe: LibraryItem | undefined
|
||||
setLinkToUnsubscribe: (set: LibraryItem | undefined) => void
|
||||
|
||||
mode: LibraryMode
|
||||
setMode: (set: LibraryMode) => void
|
||||
|
||||
actionHandler: (
|
||||
action: LinkedItemCardAction,
|
||||
item: LibraryItem | undefined
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
const DragnDropContainer = styled('div', {
|
||||
width: '100%',
|
||||
height: '80%',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: '1',
|
||||
alignSelf: 'center',
|
||||
left: 0,
|
||||
})
|
||||
|
||||
const DragnDropStyle = styled('div', {
|
||||
border: '3px dashed gray',
|
||||
backgroundColor: 'aliceblue',
|
||||
borderRadius: '5px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: '0.9',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
left: 0,
|
||||
margin: '16px',
|
||||
})
|
||||
|
||||
function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const [layout, setLayout] = usePersistedState<LayoutType>({
|
||||
key: 'libraryLayout',
|
||||
initialValue: 'GRID_LAYOUT',
|
||||
})
|
||||
const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] =
|
||||
useState(false)
|
||||
const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] =
|
||||
useState(false)
|
||||
|
||||
const updateLayout = useCallback(
|
||||
async (newLayout: LayoutType) => {
|
||||
|
|
@ -675,54 +657,74 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
[layout, setLayout]
|
||||
)
|
||||
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
height: '100%',
|
||||
width: props.mode == 'highlights' ? '100%' : 'unset',
|
||||
}}
|
||||
>
|
||||
<LibraryHeader
|
||||
layout={layout}
|
||||
updateLayout={updateLayout}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
console.log('searching with searchQuery: ', searchQuery)
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
<HStack css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryFilterMenu
|
||||
setShowAddLinkModal={props.setShowAddLinkModal}
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={(searchQuery: string) => {
|
||||
console.log('searching with searchQuery: ', searchQuery)
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
/>
|
||||
|
||||
{props.mode == 'highlights' && (
|
||||
<HighlightItemsLayout
|
||||
gridContainerRef={props.gridContainerRef}
|
||||
items={props.items}
|
||||
viewer={viewerData?.me}
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.mode == 'reads' && (
|
||||
<LibraryItemsLayout
|
||||
viewer={viewerData?.me}
|
||||
layout={layout}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type LibraryItemsLayoutProps = {
|
||||
layout: LayoutType
|
||||
viewer?: UserBasicData
|
||||
} & HomeFeedContentProps
|
||||
|
||||
function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
|
||||
const [uploadingFiles, setUploadingFiles] = useState([])
|
||||
const [inDragOperation, setInDragOperation] = useState(false)
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
|
||||
const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] =
|
||||
useState(false)
|
||||
const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] =
|
||||
useState(false)
|
||||
const [, updateState] = useState({})
|
||||
|
||||
const StyledToggleButton = styled('button', {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: '0px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '4px',
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
'&[data-state="on"]': {
|
||||
bg: 'rgb(43, 43, 43)',
|
||||
},
|
||||
})
|
||||
|
||||
const DragnDropContainer = styled('div', {
|
||||
width: '100%',
|
||||
height: '80%',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: '1',
|
||||
alignSelf: 'center',
|
||||
left: 0,
|
||||
})
|
||||
|
||||
const DragnDropStyle = styled('div', {
|
||||
border: '3px dashed gray',
|
||||
backgroundColor: 'aliceblue',
|
||||
borderRadius: '5px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: '0.9',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
left: 0,
|
||||
margin: '16px',
|
||||
})
|
||||
|
||||
const removeItem = () => {
|
||||
if (!props.linkToRemove) {
|
||||
return
|
||||
|
|
@ -742,10 +744,6 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
setShowUnsubscribeConfirmation(false)
|
||||
}
|
||||
|
||||
const [uploadingFiles, setUploadingFiles] = useState([])
|
||||
const [inDragOperation, setInDragOperation] = useState(false)
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
|
||||
const handleDrop = async (acceptedFiles: any) => {
|
||||
setInDragOperation(false)
|
||||
setUploadingFiles(acceptedFiles.map((file: { name: any }) => file.name))
|
||||
|
|
@ -795,116 +793,16 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
return (
|
||||
<>
|
||||
<VStack
|
||||
alignment="center"
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{
|
||||
px: '$3',
|
||||
width: '100%',
|
||||
'@smDown': {
|
||||
px: '$2',
|
||||
},
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Toaster />
|
||||
|
||||
{props.isValidating && props.items.length == 0 && <TopBarProgress />}
|
||||
<HStack alignment="center" distribution="start" css={{ width: '100%' }}>
|
||||
<StyledText
|
||||
style="subHeadline"
|
||||
css={{
|
||||
mr: '32px',
|
||||
'@smDown': {
|
||||
mr: '16px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Library
|
||||
</StyledText>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<StyledToggleButton
|
||||
data-state={layout === 'GRID_LAYOUT' ? 'on' : 'off'}
|
||||
onClick={() => {
|
||||
updateLayout('GRID_LAYOUT')
|
||||
}}
|
||||
>
|
||||
<GridLayoutIcon color={'rgb(211, 211, 213)'} />
|
||||
</StyledToggleButton>
|
||||
<StyledToggleButton
|
||||
data-state={layout === 'LIST_LAYOUT' ? 'on' : 'off'}
|
||||
onClick={() => {
|
||||
updateLayout('LIST_LAYOUT')
|
||||
}}
|
||||
>
|
||||
<ListLayoutIcon color={'rgb(211, 211, 213)'} />
|
||||
</StyledToggleButton>
|
||||
</Box>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ marginLeft: 'auto' }}
|
||||
onClick={() => {
|
||||
props.setShowAddLinkModal(true)
|
||||
}}
|
||||
>
|
||||
Add Link
|
||||
</Button>
|
||||
</HStack>
|
||||
<LibrarySearchBar
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={props.applySearchQuery}
|
||||
/>
|
||||
|
||||
{viewerData?.me && (
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '44px',
|
||||
marginTop: '16px',
|
||||
gap: '8px',
|
||||
flexDirection: 'row',
|
||||
overflowY: 'scroll',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.keys(SAVED_SEARCHES).map((key) => {
|
||||
const isInboxTerm = (term: string) => {
|
||||
return !term || term === 'in:inbox'
|
||||
}
|
||||
|
||||
const searchQuery = SAVED_SEARCHES[key]
|
||||
const style =
|
||||
searchQuery === props.searchTerm ||
|
||||
(!props.searchTerm && isInboxTerm(searchQuery))
|
||||
? 'ctaDarkYellow'
|
||||
: 'ctaLightGray'
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
css={{
|
||||
p: '10px 12px',
|
||||
height: '37.5px',
|
||||
borderRadius: '6px',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
<Dropzone
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={() => {
|
||||
|
|
@ -922,7 +820,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
{({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
|
||||
<div
|
||||
{...getRootProps({ className: 'dropzone' })}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
{inDragOperation && uploadingFiles.length < 1 && (
|
||||
<DragnDropContainer>
|
||||
|
|
@ -981,85 +879,21 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
ref={props.gridContainerRef}
|
||||
css={{
|
||||
py: '$3',
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridAutoRows: 'auto',
|
||||
borderRadius: '8px',
|
||||
gridGap: layout == 'LIST_LAYOUT' ? '0' : '$3',
|
||||
marginTop: layout == 'LIST_LAYOUT' ? '21px' : '0',
|
||||
marginBottom: '0px',
|
||||
paddingTop: layout == 'LIST_LAYOUT' ? '0' : '21px',
|
||||
paddingBottom: layout == 'LIST_LAYOUT' ? '0px' : '21px',
|
||||
overflow: 'hidden',
|
||||
'@smDown': {
|
||||
border: 'unset',
|
||||
width: layout == 'LIST_LAYOUT' ? '100vw' : undefined,
|
||||
margin:
|
||||
layout == 'LIST_LAYOUT' ? '16px -16px' : undefined,
|
||||
borderRadius: layout == 'LIST_LAYOUT' ? 0 : undefined,
|
||||
},
|
||||
'@md': {
|
||||
gridTemplateColumns:
|
||||
layout == 'LIST_LAYOUT' ? 'none' : '1fr 1fr',
|
||||
},
|
||||
'@lg': {
|
||||
gridTemplateColumns:
|
||||
layout == 'LIST_LAYOUT' ? 'none' : 'repeat(3, 1fr)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.items.map((linkedItem) => (
|
||||
<Box
|
||||
className="linkedItemCard"
|
||||
data-testid="linkedItemCard"
|
||||
id={linkedItem.node.id}
|
||||
tabIndex={0}
|
||||
key={linkedItem.node.id}
|
||||
css={{
|
||||
width: '100%',
|
||||
'&> div': {
|
||||
bg: '$grayBg',
|
||||
},
|
||||
'&:focus': {
|
||||
'> div': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
},
|
||||
'&:hover': {
|
||||
'> div': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{viewerData?.me && (
|
||||
<LinkedItemCard
|
||||
layout={layout}
|
||||
item={linkedItem.node}
|
||||
viewer={viewerData.me}
|
||||
handleAction={(action: LinkedItemCardAction) => {
|
||||
if (action === 'delete') {
|
||||
setShowRemoveLinkConfirmation(true)
|
||||
props.setLinkToRemove(linkedItem)
|
||||
} else if (action === 'editTitle') {
|
||||
props.setShowEditTitleModal(true)
|
||||
props.setLinkToEdit(linkedItem)
|
||||
} else if (action == 'unsubscribe') {
|
||||
setShowUnsubscribeConfirmation(true)
|
||||
props.setLinkToUnsubscribe(linkedItem)
|
||||
} else {
|
||||
props.actionHandler(action, linkedItem)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<LibraryItems
|
||||
items={props.items}
|
||||
layout={props.layout}
|
||||
viewer={props.viewer}
|
||||
gridContainerRef={props.gridContainerRef}
|
||||
setShowEditTitleModal={props.setShowEditTitleModal}
|
||||
setLinkToEdit={props.setLinkToEdit}
|
||||
setShowUnsubscribeConfirmation={
|
||||
setShowUnsubscribeConfirmation
|
||||
}
|
||||
setLinkToRemove={props.setLinkToRemove}
|
||||
setLinkToUnsubscribe={props.setLinkToUnsubscribe}
|
||||
setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation}
|
||||
actionHandler={props.actionHandler}
|
||||
/>
|
||||
)}
|
||||
<HStack
|
||||
distribution="center"
|
||||
|
|
@ -1084,21 +918,12 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
)}
|
||||
</Dropzone>
|
||||
</VStack>
|
||||
{/* Temporary code */}
|
||||
{/* <div>
|
||||
<strong>Files:</strong>
|
||||
<ul>
|
||||
{uploadingFiles.map((fileName) => (
|
||||
<li key={fileName}>{fileName}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div> */}
|
||||
{/* Temporary code */}
|
||||
|
||||
{props.showAddLinkModal && (
|
||||
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
|
||||
)}
|
||||
{props.showEditTitleModal && (
|
||||
<EditTitleModal
|
||||
<EditLibraryItemModal
|
||||
updateItem={(item: LibraryItem) =>
|
||||
props.actionHandler('update-item', item)
|
||||
}
|
||||
|
|
@ -1106,62 +931,6 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
item={props.linkToEdit as LibraryItem}
|
||||
/>
|
||||
)}
|
||||
{props.shareTarget && viewerData?.me?.profile.username && (
|
||||
<ShareArticleModal
|
||||
url={`${webBaseURL}${viewerData?.me?.profile.username}/${props.shareTarget.node.slug}/highlights?r=true`}
|
||||
title={props.shareTarget.node.title}
|
||||
imageURL={props.shareTarget.node.image}
|
||||
author={props.shareTarget.node.author}
|
||||
publishedAt={
|
||||
props.shareTarget.node.publishedAt ??
|
||||
props.shareTarget.node.createdAt
|
||||
}
|
||||
description={props.shareTarget.node.description}
|
||||
originalArticleUrl={props.shareTarget.node.originalArticleUrl}
|
||||
onOpenChange={() => {
|
||||
if (props.shareTarget) {
|
||||
const item = document.getElementById(props.shareTarget.node.id)
|
||||
if (item) {
|
||||
item.focus()
|
||||
}
|
||||
props.setShareTarget(undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{props.snoozeTarget && (
|
||||
<SnoozeLinkModal
|
||||
submit={(option: string, sendReminder: boolean, msg: string) => {
|
||||
if (!props.snoozeTarget) return
|
||||
createReminderMutation(
|
||||
props.snoozeTarget?.node.id,
|
||||
ReminderType.Tonight,
|
||||
true,
|
||||
sendReminder
|
||||
)
|
||||
.then(() => {
|
||||
return props.actionHandler('archive', props.snoozeTarget)
|
||||
})
|
||||
.then(() => {
|
||||
showSuccessToast(msg, { position: 'bottom-right' })
|
||||
})
|
||||
.catch((error) => {
|
||||
showErrorToast('There was an error snoozing your link.', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
})
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
if (props.snoozeTarget) {
|
||||
const item = document.getElementById(props.snoozeTarget.node.id)
|
||||
if (item) {
|
||||
item.focus()
|
||||
}
|
||||
props.setSnoozeTarget(undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showRemoveLinkConfirmation && (
|
||||
<ConfirmationModal
|
||||
richMessage={
|
||||
|
|
@ -1170,7 +939,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
Are you sure you want to delete this item? All associated notes
|
||||
and highlights will be deleted.
|
||||
</StyledText>
|
||||
{props.linkToRemove?.node && viewerData?.me && (
|
||||
{props.linkToRemove?.node && props.viewer && (
|
||||
<Box
|
||||
css={{
|
||||
transform: 'scale(0.6)',
|
||||
|
|
@ -1181,7 +950,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
>
|
||||
<LinkedItemCard
|
||||
item={props.linkToRemove?.node}
|
||||
viewer={viewerData.me}
|
||||
viewer={props.viewer}
|
||||
layout="GRID_LAYOUT"
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handleAction={() => {}}
|
||||
|
|
@ -1232,3 +1001,121 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type LibraryItemsProps = {
|
||||
items: LibraryItem[]
|
||||
layout: LayoutType
|
||||
viewer: UserBasicData | undefined
|
||||
|
||||
gridContainerRef: React.RefObject<HTMLDivElement>
|
||||
|
||||
setShowEditTitleModal: (show: boolean) => void
|
||||
setLinkToEdit: (set: LibraryItem | undefined) => void
|
||||
setShowUnsubscribeConfirmation: (show: true) => void
|
||||
setLinkToRemove: (set: LibraryItem | undefined) => void
|
||||
setLinkToUnsubscribe: (set: LibraryItem | undefined) => void
|
||||
setShowRemoveLinkConfirmation: (show: true) => void
|
||||
|
||||
actionHandler: (
|
||||
action: LinkedItemCardAction,
|
||||
item: LibraryItem | undefined
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
function LibraryItems(props: LibraryItemsProps): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
ref={props.gridContainerRef}
|
||||
css={{
|
||||
py: '$3',
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridAutoRows: 'auto',
|
||||
borderRadius: '8px',
|
||||
gridGap: props.layout == 'LIST_LAYOUT' ? '0' : '20px',
|
||||
marginTop: props.layout == 'LIST_LAYOUT' ? '21px' : '0',
|
||||
marginBottom: '0px',
|
||||
paddingTop: props.layout == 'LIST_LAYOUT' ? '0' : '21px',
|
||||
paddingBottom: props.layout == 'LIST_LAYOUT' ? '0px' : '21px',
|
||||
overflow: 'hidden',
|
||||
'@xlgDown': {
|
||||
border: 'unset',
|
||||
borderRadius: props.layout == 'LIST_LAYOUT' ? 0 : undefined,
|
||||
},
|
||||
'@smDown': {
|
||||
border: 'unset',
|
||||
width: props.layout == 'LIST_LAYOUT' ? '100vw' : undefined,
|
||||
margin: props.layout == 'LIST_LAYOUT' ? '16px -16px' : undefined,
|
||||
borderRadius: props.layout == 'LIST_LAYOUT' ? 0 : undefined,
|
||||
},
|
||||
'@md': {
|
||||
gridTemplateColumns:
|
||||
props.layout == 'LIST_LAYOUT' ? 'none' : '1fr 1fr',
|
||||
},
|
||||
'@lg': {
|
||||
gridTemplateColumns:
|
||||
props.layout == 'LIST_LAYOUT' ? 'none' : 'repeat(2, 1fr)',
|
||||
},
|
||||
'@xl': {
|
||||
gridTemplateColumns:
|
||||
props.layout == 'LIST_LAYOUT' ? 'none' : 'repeat(3, 1fr)',
|
||||
},
|
||||
'@xxl': {
|
||||
gridTemplateColumns:
|
||||
props.layout == 'LIST_LAYOUT' ? 'none' : 'repeat(4, 1fr)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.items.map((linkedItem) => (
|
||||
<Box
|
||||
className="linkedItemCard"
|
||||
data-testid="linkedItemCard"
|
||||
id={linkedItem.node.id}
|
||||
tabIndex={0}
|
||||
key={linkedItem.node.id}
|
||||
css={{
|
||||
width: '100%',
|
||||
'&> div': {
|
||||
bg: '$thBackground3',
|
||||
},
|
||||
'&:focus': {
|
||||
'> div': {
|
||||
bg: '$thBackgroundActive',
|
||||
},
|
||||
},
|
||||
'&:hover': {
|
||||
'> div': {
|
||||
bg: '$thBackgroundActive',
|
||||
},
|
||||
'> a': {
|
||||
bg: '$thBackgroundActive',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.viewer && (
|
||||
<LinkedItemCard
|
||||
layout={props.layout}
|
||||
item={linkedItem.node}
|
||||
viewer={props.viewer}
|
||||
handleAction={(action: LinkedItemCardAction) => {
|
||||
if (action === 'delete') {
|
||||
props.setShowRemoveLinkConfirmation(true)
|
||||
props.setLinkToRemove(linkedItem)
|
||||
} else if (action === 'editTitle') {
|
||||
props.setShowEditTitleModal(true)
|
||||
props.setLinkToEdit(linkedItem)
|
||||
} else if (action == 'unsubscribe') {
|
||||
props.setShowUnsubscribeConfirmation(true)
|
||||
props.setLinkToUnsubscribe(linkedItem)
|
||||
} else {
|
||||
props.actionHandler(action, linkedItem)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
494
packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx
Normal file
494
packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
import { ReactNode, useMemo, useState } from 'react'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { CaretRight, Circle, DotsThree, Plus } from 'phosphor-react'
|
||||
import { 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 { currentThemeName } from '../../../lib/themeUpdater'
|
||||
import { MOBILE_HEADER_HEIGHT } from './HeaderSpacer'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '300px'
|
||||
|
||||
type LibraryFilterMenuProps = {
|
||||
setShowAddLinkModal: (show: boolean) => void
|
||||
|
||||
searchTerm: string | undefined
|
||||
applySearchQuery: (searchTerm: string) => void
|
||||
|
||||
showFilterMenu: boolean
|
||||
setShowFilterMenu: (show: boolean) => void
|
||||
}
|
||||
|
||||
export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
left: '0px',
|
||||
top: '105px',
|
||||
position: 'fixed',
|
||||
bg: '$thBackground',
|
||||
height: '100%',
|
||||
width: LIBRARY_LEFT_MENU_WIDTH,
|
||||
borderRight: '1px solid $thBorderColor',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
'@mdDown': {
|
||||
visibility: props.showFilterMenu ? 'visible' : 'hidden',
|
||||
top: props.showFilterMenu ? MOBILE_HEADER_HEIGHT : '100%',
|
||||
width: '100%',
|
||||
transition: 'visibility 0s, top 150ms',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SavedSearches {...props} />
|
||||
<Subscriptions {...props} />
|
||||
<Labels {...props} />
|
||||
|
||||
<AddLinkButton
|
||||
showAddLinkModal={() => props.setShowAddLinkModal(true)}
|
||||
/>
|
||||
</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): JSX.Element {
|
||||
const items = [
|
||||
{
|
||||
name: 'Inbox',
|
||||
term: 'in:inbox',
|
||||
},
|
||||
{
|
||||
name: 'Read Later',
|
||||
term: 'in:inbox -label:Newsletter',
|
||||
},
|
||||
{
|
||||
name: 'Highlights',
|
||||
term: 'has:highlights mode:highlights',
|
||||
},
|
||||
{
|
||||
name: 'Unlabeled',
|
||||
term: 'no:label',
|
||||
},
|
||||
{
|
||||
name: 'Files',
|
||||
term: 'type:file',
|
||||
},
|
||||
{
|
||||
name: 'Archived',
|
||||
term: 'in:archive',
|
||||
},
|
||||
]
|
||||
|
||||
useRegisterActions(
|
||||
items.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.term)
|
||||
},
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenuPanel title="Saved Searches">
|
||||
{items.map((item) => (
|
||||
<FilterButton
|
||||
key={item.name}
|
||||
text={item.name}
|
||||
filterTerm={item.term}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Box css={{ height: '10px' }}></Box>
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function Subscriptions(props: LibraryFilterMenuProps): JSX.Element {
|
||||
const { subscriptions } = useGetSubscriptionsQuery()
|
||||
const [viewAll, setViewAll] = useState(false)
|
||||
|
||||
useRegisterActions(
|
||||
subscriptions.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}\"`)
|
||||
},
|
||||
}
|
||||
}),
|
||||
[subscriptions]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenuPanel
|
||||
title="Subscriptions"
|
||||
editTitle="Edit Subscriptions"
|
||||
editFunc={() => {
|
||||
window.location.href = '/settings/subscriptions'
|
||||
}}
|
||||
>
|
||||
{subscriptions.slice(0, viewAll ? undefined : 4).map((item) => {
|
||||
return (
|
||||
<FilterButton
|
||||
key={item.id}
|
||||
filterTerm={`subscription:\"${item.name}\"`}
|
||||
text={item.name}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<ViewAllButton state={viewAll} setState={setViewAll} />
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function Labels(props: LibraryFilterMenuProps): JSX.Element {
|
||||
const { labels } = useGetLabelsQuery()
|
||||
const [viewAll, setViewAll] = useState(false)
|
||||
|
||||
return (
|
||||
<MenuPanel
|
||||
title="Labels"
|
||||
editTitle="Edit Labels"
|
||||
editFunc={() => {
|
||||
window.location.href = '/settings/labels'
|
||||
}}
|
||||
>
|
||||
{labels.slice(0, viewAll ? undefined : 4).map((item) => {
|
||||
return <LabelButton key={item.id} label={item} {...props} />
|
||||
})}
|
||||
<ViewAllButton state={viewAll} setState={setViewAll} />
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
type MenuPanelProps = {
|
||||
title: string
|
||||
children: ReactNode
|
||||
editFunc?: () => void
|
||||
editTitle?: string
|
||||
}
|
||||
|
||||
function MenuPanel(props: MenuPanelProps): JSX.Element {
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
m: '0px',
|
||||
width: '100%',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
px: '15px',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<HStack css={{ width: '100%' }} distribution="start" alignment="start">
|
||||
<StyledText
|
||||
css={{
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
lineHeight: '125%',
|
||||
color: '$thTextContrast',
|
||||
pl: '10px',
|
||||
my: '20px',
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</StyledText>
|
||||
<SpanBox
|
||||
css={{
|
||||
my: '15px',
|
||||
marginLeft: 'auto',
|
||||
height: '100%',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
{props.editTitle && props.editFunc && (
|
||||
<Dropdown
|
||||
triggerElement={
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '30px',
|
||||
width: '30px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
'&:hover': {
|
||||
bg: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DotsThree
|
||||
size={25}
|
||||
weight="bold"
|
||||
color={theme.colors.thTextSubtle2.toString()}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<DropdownOption
|
||||
title={props.editTitle}
|
||||
onSelect={() => {
|
||||
if (props.editFunc) {
|
||||
props.editFunc()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</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 ? '#FFEA9F' : 'unset',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: selected ? '#3D3D3D' : '$thTextSubtle',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
backgroundColor: selected ? '#FFEA9F' : '$thBackground4',
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: selected ? '#FFEA9F' : '$thBackground4',
|
||||
},
|
||||
}}
|
||||
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 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: '16px',
|
||||
fontWeight: 'regular',
|
||||
color: '$thTextSubtle',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
m: '0px',
|
||||
'&:hover': {
|
||||
backgroundColor: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
>
|
||||
<label htmlFor={labelId} style={{ width: '100%' }}>
|
||||
<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}
|
||||
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 AddLinkButtonProps = {
|
||||
showAddLinkModal: () => void
|
||||
}
|
||||
|
||||
function AddLinkButton(props: AddLinkButtonProps): JSX.Element {
|
||||
const currentTheme = currentThemeName()
|
||||
const isDark = currentTheme == 'Dark'
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack
|
||||
css={{
|
||||
marginTop: 'auto',
|
||||
width: LIBRARY_LEFT_MENU_WIDTH,
|
||||
height: '80px',
|
||||
pl: '25px',
|
||||
}}
|
||||
distribution="center"
|
||||
>
|
||||
<Button
|
||||
css={{
|
||||
height: '40px',
|
||||
p: '15px',
|
||||
pr: '20px',
|
||||
fontSize: '14px',
|
||||
verticalAlign: 'center',
|
||||
color: isDark
|
||||
? theme.colors.thHighContrast.toString()
|
||||
: theme.colors.thTextContrast2.toString(),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: '600',
|
||||
bg: isDark ? 'transparent' : theme.colors.thBackground2.toString(),
|
||||
border: `1px solid ${
|
||||
isDark ? theme.colors.thHighContrast.toString() : 'transparent'
|
||||
}`,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
props.showAddLinkModal()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<Plus size={16} weight="bold" />
|
||||
<SpanBox css={{ width: '10px' }}></SpanBox>Add Link
|
||||
</Button>
|
||||
</VStack>
|
||||
<Box css={{ height: '180px ' }} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type ViewAllButtonProps = {
|
||||
state: boolean
|
||||
setState: (state: boolean) => void
|
||||
}
|
||||
|
||||
function ViewAllButton(props: ViewAllButtonProps): JSX.Element {
|
||||
return (
|
||||
<Button
|
||||
style="ghost"
|
||||
css={{
|
||||
display: 'flex',
|
||||
pl: '10px',
|
||||
color: '#898989',
|
||||
fontWeight: '600',
|
||||
fontSize: '12px',
|
||||
py: '20px',
|
||||
gap: '2px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
props.setState(!props.state)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.state ? 'Hide' : 'View All'}
|
||||
{props.state ? null : (
|
||||
<CaretRight size={12} color="#898989" weight="bold" />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
414
packages/web/components/templates/homeFeed/LibraryHeader.tsx
Normal file
414
packages/web/components/templates/homeFeed/LibraryHeader.tsx
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
import { useRef, useState } from 'react'
|
||||
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { Button, IconButton } from '../../elements/Button'
|
||||
import { FunnelSimple, MagnifyingGlass, X } from 'phosphor-react'
|
||||
import { ListSelectorIcon } from '../../elements/images/ListSelectorIcon'
|
||||
import { GridSelectorIcon } from '../../elements/images/GridSelectorIcon'
|
||||
import { LayoutType } from './HomeFeedContainer'
|
||||
import { PrimaryDropdown } from '../PrimaryDropdown'
|
||||
import { LogoBox } from '../../elements/LogoBox'
|
||||
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
|
||||
import {
|
||||
HeaderSpacer,
|
||||
HEADER_HEIGHT,
|
||||
MOBILE_HEADER_HEIGHT,
|
||||
} from './HeaderSpacer'
|
||||
|
||||
type LibraryHeaderProps = {
|
||||
layout: LayoutType
|
||||
updateLayout: (layout: LayoutType) => void
|
||||
|
||||
searchTerm: string | undefined
|
||||
applySearchQuery: (searchQuery: string) => void
|
||||
|
||||
showFilterMenu: boolean
|
||||
setShowFilterMenu: (show: boolean) => void
|
||||
}
|
||||
|
||||
export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: 5,
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
height: HEADER_HEIGHT,
|
||||
bg: '$thBackground',
|
||||
pt: '35px',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
'@mdDown': {
|
||||
height: MOBILE_HEADER_HEIGHT,
|
||||
pt: '0px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* These will display/hide depending on breakpoints */}
|
||||
<LargeHeaderLayout {...props} />
|
||||
<SmallHeaderLayout {...props} />
|
||||
</VStack>
|
||||
|
||||
{/* This spacer is put in to push library content down
|
||||
below the fixed header height. */}
|
||||
<HeaderSpacer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoBox />
|
||||
<SearchBox {...props} />
|
||||
<ControlButtonBox
|
||||
layout={props.layout}
|
||||
updateLayout={props.updateLayout}
|
||||
/>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
function SmallHeaderLayout(props: LibraryHeaderProps): JSX.Element {
|
||||
const [showInlineSearch, setShowInlineSearch] = useState(false)
|
||||
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bg: '$thBackground3',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{showInlineSearch ? (
|
||||
<HStack css={{ pl: '10px', pr: '0px', width: '100%' }}>
|
||||
<SearchBox {...props} compact={true} />
|
||||
<Button
|
||||
style="cancelGeneric"
|
||||
onClick={(event) => {
|
||||
setShowInlineSearch(false)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</HStack>
|
||||
) : (
|
||||
<>
|
||||
<MenuHeaderButton {...props} />
|
||||
<ControlButtonBox
|
||||
layout={props.layout}
|
||||
updateLayout={props.updateLayout}
|
||||
setShowInlineSearch={setShowInlineSearch}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type MenuHeaderButtonProps = {
|
||||
showFilterMenu: boolean
|
||||
setShowFilterMenu: (show: boolean) => void
|
||||
}
|
||||
|
||||
export function MenuHeaderButton(props: MenuHeaderButtonProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
ml: '10px',
|
||||
width: '67px',
|
||||
height: '40px',
|
||||
bg: props.showFilterMenu ? '$thTextContrast2' : '$thBackground2',
|
||||
borderRadius: '5px',
|
||||
px: '5px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="around"
|
||||
onClick={() => {
|
||||
props.setShowFilterMenu(!props.showFilterMenu)
|
||||
}}
|
||||
>
|
||||
<OmnivoreSmallLogo
|
||||
size={20}
|
||||
strokeColor={
|
||||
props.showFilterMenu
|
||||
? theme.colors.thBackground.toString()
|
||||
: theme.colors.thTextContrast2.toString()
|
||||
}
|
||||
/>
|
||||
<FunnelSimple
|
||||
size={20}
|
||||
color={
|
||||
props.showFilterMenu
|
||||
? theme.colors.thBackground.toString()
|
||||
: theme.colors.thTextContrast2.toString()
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
export type SearchBoxProps = {
|
||||
searchTerm: string | undefined
|
||||
applySearchQuery: (searchQuery: string) => void
|
||||
|
||||
compact?: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function SearchBox(props: SearchBoxProps): JSX.Element {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [focused, setFocused] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState(props.searchTerm ?? '')
|
||||
|
||||
const border = props.compact
|
||||
? focused
|
||||
? '1px solid $omnivoreCtaYellow'
|
||||
: '1px solid black'
|
||||
: focused
|
||||
? '1px solid $omnivoreCtaYellow'
|
||||
: '1px solid $thBorderColor'
|
||||
|
||||
useKeyboardShortcuts(
|
||||
searchBarCommands((action) => {
|
||||
if (action === 'focusSearchBar' && inputRef.current) {
|
||||
inputRef.current.select()
|
||||
}
|
||||
if (action == 'clearSearch' && inputRef.current) {
|
||||
setSearchTerm('')
|
||||
props.applySearchQuery('')
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
maxWidth: '521px',
|
||||
bg: props.compact ? 'white' : '$thBackground2',
|
||||
borderRadius: '6px',
|
||||
border: border,
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
height: '100%',
|
||||
pl: props.compact ? '10px' : '15px',
|
||||
pr: props.compact ? '5px' : '10px',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
inputRef.current?.focus()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<MagnifyingGlass
|
||||
size={props.compact ? 15 : 20}
|
||||
color={theme.colors.graySolid.toString()}
|
||||
/>
|
||||
</HStack>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
props.applySearchQuery(searchTerm || '')
|
||||
inputRef.current?.blur()
|
||||
if (props.onClose) {
|
||||
props.onClose()
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<FormInput
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
autoFocus={!!props.compact}
|
||||
placeholder="Search keywords or labels"
|
||||
onFocus={(event) => {
|
||||
event.target.select()
|
||||
setFocused(true)
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false)
|
||||
}}
|
||||
onChange={(event) => {
|
||||
setSearchTerm(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
const key = event.key.toLowerCase()
|
||||
if (key == 'escape') {
|
||||
event.currentTarget.blur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
{searchTerm && searchTerm.length ? (
|
||||
<Box
|
||||
css={{
|
||||
py: '15px',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style="searchButton"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
setSearchTerm('')
|
||||
props.applySearchQuery('')
|
||||
inputRef.current?.blur()
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<X
|
||||
width={16}
|
||||
height={16}
|
||||
color={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
css={{
|
||||
py: '15px',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style="searchButton"
|
||||
onClick={() =>
|
||||
requestAnimationFrame(() => inputRef?.current?.focus())
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<kbd aria-hidden>/</kbd>
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type ControlButtonBoxProps = {
|
||||
layout: LayoutType
|
||||
updateLayout: (layout: LayoutType) => void
|
||||
setShowInlineSearch?: (show: boolean) => void
|
||||
}
|
||||
|
||||
function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: '45px',
|
||||
width: '100px',
|
||||
height: '100%',
|
||||
gap: '20px',
|
||||
minWidth: '121px',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ display: 'flex' }}
|
||||
onClick={(e) => {
|
||||
props.updateLayout('LIST_LAYOUT')
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<ListSelectorIcon
|
||||
color={props.layout == 'GRID_LAYOUT' ? '#6A6968' : '#FFEA9F'}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ display: 'flex' }}
|
||||
onClick={(e) => {
|
||||
props.updateLayout('GRID_LAYOUT')
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<GridSelectorIcon
|
||||
color={props.layout == 'LIST_LAYOUT' ? '#6A6968' : '#FFEA9F'}
|
||||
/>
|
||||
</Button>
|
||||
<PrimaryDropdown showThemeSection={true} />
|
||||
</HStack>
|
||||
|
||||
{props.setShowInlineSearch && (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: '20px',
|
||||
width: '100px',
|
||||
height: '100%',
|
||||
gap: '20px',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.setShowInlineSearch && props.setShowInlineSearch(true)
|
||||
}}
|
||||
css={{
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<MagnifyingGlass
|
||||
size={20}
|
||||
color={theme.colors.graySolid.toString()}
|
||||
/>
|
||||
</Button>
|
||||
<PrimaryDropdown
|
||||
showThemeSection={true}
|
||||
layout={props.layout}
|
||||
updateLayout={props.updateLayout}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -40,14 +40,6 @@ export function LibrarySearchBar(props: LibrarySearchBarProps): JSX.Element {
|
|||
setSearchTerm(props.searchTerm || '')
|
||||
}, [props.searchTerm])
|
||||
|
||||
useKeyboardShortcuts(
|
||||
searchBarCommands((action) => {
|
||||
if (action === 'focusSearchBar' && inputRef.current) {
|
||||
inputRef.current.select()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%' }}>
|
||||
<HStack
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { styled } from '@stitches/react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
|
|
@ -9,7 +8,10 @@ import { StyledText } from '../../elements/StyledText'
|
|||
import { FormInput } from '../../elements/FormElements'
|
||||
|
||||
import { setIntegrationMutation } from '../../../lib/networking/mutations/setIntegrationMutation'
|
||||
import { Integration, useGetIntegrationsQuery } from '../../../lib/networking/queries/useGetIntegrationsQuery'
|
||||
import {
|
||||
Integration,
|
||||
useGetIntegrationsQuery,
|
||||
} from '../../../lib/networking/queries/useGetIntegrationsQuery'
|
||||
import { useRouter } from 'next/router'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { deleteIntegrationMutation } from '../../../lib/networking/mutations/deleteIntegrationMutation'
|
||||
|
|
@ -22,7 +24,7 @@ const Header = styled(Box, {
|
|||
})
|
||||
|
||||
export function Readwise(): JSX.Element {
|
||||
const { integrations, isValidating, revalidate } = useGetIntegrationsQuery()
|
||||
const { integrations, revalidate } = useGetIntegrationsQuery()
|
||||
const readwiseIntegration = useMemo(() => {
|
||||
return integrations.find((i) => i.type == 'READWISE')
|
||||
}, [integrations])
|
||||
|
|
@ -57,23 +59,23 @@ export function Readwise(): JSX.Element {
|
|||
</HStack>
|
||||
|
||||
{readwiseIntegration && (
|
||||
<RemoveReadwiseForm integration={readwiseIntegration} revalidate={revalidate} />
|
||||
)}
|
||||
|
||||
{!readwiseIntegration && (
|
||||
<AddReadwiseForm />
|
||||
<RemoveReadwiseForm
|
||||
integration={readwiseIntegration}
|
||||
revalidate={revalidate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!readwiseIntegration && <AddReadwiseForm />}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function AddReadwiseForm(): JSX.Element {
|
||||
const router = useRouter()
|
||||
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [token, setToken] = useState<string>('')
|
||||
|
||||
const setReadwiseToken = useCallback(async () => {
|
||||
|
|
@ -94,64 +96,64 @@ function AddReadwiseForm(): JSX.Element {
|
|||
}
|
||||
}, [token, router])
|
||||
|
||||
return (<>
|
||||
<HStack
|
||||
css={{
|
||||
fontSize: '18px',
|
||||
color: '$utilityTextDefault',
|
||||
my: '20px',
|
||||
whiteSpace: 'pre-wrap'
|
||||
}}
|
||||
>
|
||||
<SpanBox>Enter your API key from Readwise below. You can get your token{' '}
|
||||
<a
|
||||
target='_blank'
|
||||
rel="noreferrer"
|
||||
referrerPolicy='no-referrer'
|
||||
style={{ color: '$utilityTextDefault' }}
|
||||
href="https://readwise.io/access_token"
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
css={{
|
||||
fontSize: '18px',
|
||||
color: '$utilityTextDefault',
|
||||
my: '20px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
here
|
||||
</a>.</SpanBox>
|
||||
</HStack>
|
||||
<SpanBox>
|
||||
Enter your API key from Readwise below. You can get your token{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
referrerPolicy="no-referrer"
|
||||
style={{ color: '$utilityTextDefault' }}
|
||||
href="https://readwise.io/access_token"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
|
||||
<FormInput
|
||||
type="token"
|
||||
key="token"
|
||||
value={token}
|
||||
placeholder={'Readwise Token'}
|
||||
onChange={(e) => {
|
||||
e.preventDefault()
|
||||
setToken(e.target.value)
|
||||
}}
|
||||
disabled={false}
|
||||
hidden={false}
|
||||
required={true}
|
||||
css={{
|
||||
border: '1px solid $textNonessential',
|
||||
borderRadius: '8px',
|
||||
width: '80%',
|
||||
bg: 'transparent',
|
||||
fontSize: '16px',
|
||||
textIndent: '8px',
|
||||
my: '20px',
|
||||
height: '38px',
|
||||
color: '$grayTextContrast',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
|
||||
},
|
||||
}}
|
||||
min={200}
|
||||
/>
|
||||
{errorMessage && <StyledText style="error">{errorMessage}</StyledText>}
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ }}
|
||||
onClick={setReadwiseToken}
|
||||
>
|
||||
Set Token
|
||||
</Button>
|
||||
<FormInput
|
||||
type="token"
|
||||
key="token"
|
||||
value={token}
|
||||
placeholder={'Readwise Token'}
|
||||
onChange={(e) => {
|
||||
e.preventDefault()
|
||||
setToken(e.target.value)
|
||||
}}
|
||||
disabled={false}
|
||||
hidden={false}
|
||||
required={true}
|
||||
css={{
|
||||
border: '1px solid $textNonessential',
|
||||
borderRadius: '8px',
|
||||
width: '80%',
|
||||
bg: 'transparent',
|
||||
fontSize: '16px',
|
||||
textIndent: '8px',
|
||||
my: '20px',
|
||||
height: '38px',
|
||||
color: '$grayTextContrast',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
|
||||
},
|
||||
}}
|
||||
min={200}
|
||||
/>
|
||||
{errorMessage && <StyledText style="error">{errorMessage}</StyledText>}
|
||||
<Button style="ctaDarkYellow" css={{}} onClick={setReadwiseToken}>
|
||||
Set Token
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -179,19 +181,23 @@ function RemoveReadwiseForm(props: RemoveReadwiseFormProps): JSX.Element {
|
|||
}, [props])
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
css={{
|
||||
fontSize: '18px',
|
||||
color: '$utilityTextDefault',
|
||||
my: '20px',
|
||||
whiteSpace: 'pre-wrap'
|
||||
}}
|
||||
>
|
||||
<SpanBox>Omnivore is configured to send all your highlights to Readwise.</SpanBox>
|
||||
</HStack>
|
||||
<>
|
||||
<HStack
|
||||
css={{
|
||||
fontSize: '18px',
|
||||
color: '$utilityTextDefault',
|
||||
my: '20px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
<SpanBox>
|
||||
Omnivore is configured to send all your highlights to Readwise.
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
|
||||
<Button style="ctaDarkYellow" onClick={deleteIntegration}>Remove Readwise Integration</Button>
|
||||
</>
|
||||
<Button style="ctaDarkYellow" onClick={deleteIntegration}>
|
||||
Remove Readwise Integration
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import Link from 'next/link'
|
||||
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { GithubLogo, DiscordLogo, TwitterLogo } from 'phosphor-react'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { styled } from '../../tokens/stitches.config'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
|
||||
const containerStyles = {
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
import { SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
|
||||
import { styled } from '../../tokens/stitches.config'
|
||||
import { Root, Image, Fallback } from '@radix-ui/react-avatar'
|
||||
|
||||
type AvatarProps = {
|
||||
viewer?: UserBasicData
|
||||
}
|
||||
|
||||
export function LibraryAvatar(props: AvatarProps): JSX.Element {
|
||||
return (
|
||||
<VStack alignment="center" distribution="start" css={{ pl: '8px', width: '100%', height: '100%' }}>
|
||||
<VStack css={{ height: '100%' }}>
|
||||
<StyledAvatar
|
||||
css={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
{props.viewer?.profile.pictureUrl
|
||||
? <StyledImage src={props.viewer.profile.pictureUrl} />
|
||||
: <StyledFallback>{props.viewer?.name.charAt(0) ?? ''}</StyledFallback>
|
||||
}
|
||||
</StyledAvatar>
|
||||
</VStack>
|
||||
{/* This spacer is to help align with items in the search box */}
|
||||
<SpanBox css={{ marginTop: 'auto', height: '10px', width: '100%' }} />
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledAvatar = styled(Root, {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
verticalAlign: 'middle',
|
||||
overflow: 'hidden',
|
||||
userSelect: 'none',
|
||||
})
|
||||
|
||||
const StyledImage = styled(Image, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
|
||||
'&:hover': {
|
||||
opacity: '48%',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledFallback = styled(Fallback, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '$utilityTextDefault',
|
||||
backgroundColor: '$libraryActiveMenuItem',
|
||||
})
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives'
|
||||
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
|
||||
import { LibraryMenu } from './LibraryMenu'
|
||||
import { LibraryAvatar } from './LibraryAvatar'
|
||||
import { LibrarySearchBar } from './LibrarySearchBar'
|
||||
import { LibraryList } from './LibraryList'
|
||||
import { LibraryHeadline } from './LibraryHeadline'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { LibraryItemsQueryInput } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
|
||||
|
||||
|
||||
export type SearchCoordinator = {
|
||||
applySearch: (searchTerm: string) => void
|
||||
}
|
||||
|
||||
const useSearchCoordinator = () => {
|
||||
const applySearch = useCallback((searchTerm: string) => {
|
||||
console.log('applying search')
|
||||
}, [])
|
||||
|
||||
return {
|
||||
applySearch
|
||||
}
|
||||
}
|
||||
|
||||
export type LibraryLayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
|
||||
|
||||
export type LayoutCoordinator = {
|
||||
layout: LibraryLayoutType
|
||||
setLayout: (type: LibraryLayoutType) => void
|
||||
}
|
||||
|
||||
const useLibraryLayoutCoordinator = () => {
|
||||
const [layout, setLayout] = usePersistedState<LibraryLayoutType>({
|
||||
key: 'libraryLayout',
|
||||
initialValue: 'GRID_LAYOUT',
|
||||
})
|
||||
|
||||
return {
|
||||
layout,
|
||||
setLayout
|
||||
}
|
||||
}
|
||||
|
||||
export function LibraryContainer(): JSX.Element {
|
||||
useGetUserPreferences()
|
||||
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const searchCoordinator = useSearchCoordinator()
|
||||
const layoutCoordinator = useLibraryLayoutCoordinator()
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack alignment="start" distribution="start" css={{ width: '100vw', height: '100vh', overflow: 'hidden', bg: '$libraryBackground' }}>
|
||||
<HStack alignment="start" distribution="start" css={{ width: '100%' }}>
|
||||
<SpanBox css={{ width: '100%', height: '90px' }}>
|
||||
<LibrarySearchBar coordinator={searchCoordinator} />
|
||||
</SpanBox>
|
||||
<SpanBox css={{ marginLeft: 'auto', width: '130px', height: '100%' }}>
|
||||
<LibraryAvatar viewer={viewerData?.me} />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
<HStack alignment="start" distribution="start" css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryMenu />
|
||||
<VStack alignment="start" distribution="start" css={{ width: '100%', height: '100%', mr: '20px' }}>
|
||||
<LibraryHeadline layoutCoordinator={layoutCoordinator} />
|
||||
<LibraryList layoutCoordinator={layoutCoordinator} />
|
||||
</VStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { LibraryListLayoutIcon } from '../../elements/images/LibraryListLayoutIcon'
|
||||
import { LibraryGridLayoutIcon } from '../../elements/images/LibraryGridLayoutIcon'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { LayoutCoordinator, LibraryLayoutType } from './LibraryContainer'
|
||||
import { useCallback } from 'react'
|
||||
import { Plus } from 'phosphor-react'
|
||||
|
||||
export type LibraryHeadlineProps = {
|
||||
layoutCoordinator: LayoutCoordinator
|
||||
}
|
||||
|
||||
export function LibraryHeadline(props: LibraryHeadlineProps): JSX.Element {
|
||||
|
||||
const typeColor = useCallback((type: LibraryLayoutType) => {
|
||||
return (
|
||||
props.layoutCoordinator.layout === type
|
||||
? theme.colors.omnivoreCtaYellow.toString()
|
||||
: "#D6D6D6"
|
||||
)
|
||||
}, [props.layoutCoordinator.layout])
|
||||
|
||||
return (
|
||||
<HStack alignment="center" distribution="start" css={{ pt: '8px', pb: '14px', width: '100%', pr: '15px' }}>
|
||||
<StyledText style="libraryHeader">Home</StyledText>
|
||||
<HStack alignment="center" distribution="start" css={{ marginLeft: 'auto', gap: '8px' }}>
|
||||
<Button style="ctaDarkYellow" css={{ py: '10px', px: '14px', mr: '16px', display: 'flex', alignItems: 'center' }}>
|
||||
<Plus size={16} weight='bold' />
|
||||
<SpanBox css={{ pl: '10px', fontWeight: '600', fontSize: '16px' }}>Add Link</SpanBox>
|
||||
</Button>
|
||||
<Button style="ghost" onClick={() => props.layoutCoordinator.setLayout('LIST_LAYOUT')} css={{ display: 'flex', alignItems: 'center' }}>
|
||||
<LibraryListLayoutIcon color={typeColor('LIST_LAYOUT')} />
|
||||
</Button>
|
||||
<Button style="ghost" onClick={() => props.layoutCoordinator.setLayout('GRID_LAYOUT')} css={{ display: 'flex', alignItems: 'center' }}>
|
||||
<LibraryGridLayoutIcon color={typeColor('GRID_LAYOUT')} />
|
||||
</Button>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
|
||||
import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes'
|
||||
import { LibraryGridCard } from '../../patterns/LibraryCards/LibraryGridCard'
|
||||
import { LayoutCoordinator } from './LibraryContainer'
|
||||
import { EmptyLibrary } from '../homeFeed/EmptyLibrary'
|
||||
import Masonry from 'react-masonry-css'
|
||||
|
||||
export type LibraryListProps = {
|
||||
layoutCoordinator: LayoutCoordinator
|
||||
}
|
||||
|
||||
export function LibraryList(props: LibraryListProps): JSX.Element {
|
||||
useGetUserPreferences()
|
||||
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
|
||||
const defaultQuery = {
|
||||
limit: 50,
|
||||
sortDescending: true,
|
||||
searchQuery: undefined,
|
||||
}
|
||||
|
||||
const { itemsPages, size, setSize, isValidating, performActionOnItem } =
|
||||
useGetLibraryItemsQuery(defaultQuery)
|
||||
|
||||
const [fileNames, setFileNames] = useState([])
|
||||
const [inDragOperation, setInDragOperation] = useState(false)
|
||||
const [uploadingFiles, setUploadingFiles] = useState([])
|
||||
|
||||
const handleDrop = (acceptedFiles: any) => {
|
||||
setFileNames(acceptedFiles.map((file: { name: any }) => file.name))
|
||||
setUploadingFiles(acceptedFiles.map((file: { name: any }) => file.name))
|
||||
}
|
||||
|
||||
const libraryItems = useMemo(() => {
|
||||
const items =
|
||||
itemsPages?.flatMap((ad) => {
|
||||
return ad.search.edges
|
||||
}) || []
|
||||
return items
|
||||
}, [itemsPages, performActionOnItem])
|
||||
|
||||
if (!isValidating && libraryItems.length == 0) {
|
||||
return (
|
||||
<EmptyLibrary
|
||||
onAddLinkClicked={() => {
|
||||
console.log('onAddLinkClicked')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
console.log(fileNames)
|
||||
|
||||
return (
|
||||
<Box css={{ overflowY: 'scroll' }}>
|
||||
{inDragOperation && uploadingFiles.length < 1 && (
|
||||
<Box
|
||||
css={{
|
||||
border: '3px dashed gray',
|
||||
backgroundColor: 'aliceblue',
|
||||
borderRadius: '5px',
|
||||
width: '75%',
|
||||
height: '70%',
|
||||
position: 'absolute',
|
||||
opacity: '0.8',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
}}
|
||||
>
|
||||
Drag n drop files here
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Dropzone
|
||||
onDrop={handleDrop}
|
||||
preventDropOnDocument={true}
|
||||
onDragEnter={() => {
|
||||
setInDragOperation(true)
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setInDragOperation(false)
|
||||
}}
|
||||
noClick={true}
|
||||
noDragEventsBubbling={true}
|
||||
>
|
||||
{({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
|
||||
<Box
|
||||
{...getRootProps({ className: 'dropzone' })}
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<Masonry
|
||||
breakpointCols={
|
||||
props.layoutCoordinator.layout == 'LIST_LAYOUT'
|
||||
? 1
|
||||
: {
|
||||
default: 3,
|
||||
1200: 2,
|
||||
992: 1,
|
||||
}
|
||||
}
|
||||
className="omnivore-masonry-grid"
|
||||
columnClassName="omnivore-masonry-grid_column"
|
||||
>
|
||||
{libraryItems.map((linkedItem) => (
|
||||
<Box
|
||||
className="linkedItemCard"
|
||||
data-testid="linkedItemCard"
|
||||
id={linkedItem.node.id}
|
||||
tabIndex={0}
|
||||
key={linkedItem.node.id}
|
||||
css={{
|
||||
width: '100%',
|
||||
'&> div': {
|
||||
bg: '$libraryBackground',
|
||||
},
|
||||
'&:focus': {
|
||||
'> div': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
},
|
||||
'&:hover': {
|
||||
'> div': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{viewerData?.me && (
|
||||
<LibraryGridCard
|
||||
layout={props.layoutCoordinator.layout}
|
||||
item={linkedItem.node}
|
||||
viewer={viewerData.me}
|
||||
handleAction={(action: LinkedItemCardAction) => {
|
||||
console.log('card clicked')
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Masonry>
|
||||
</Box>
|
||||
)}
|
||||
</Dropzone>
|
||||
{/* Temporary code */}
|
||||
<div>
|
||||
<strong>Files:</strong>
|
||||
<ul>
|
||||
{fileNames.map((fileName) => (
|
||||
<li key={fileName}>{fileName}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>{' '}
|
||||
{/* Temporary code */}
|
||||
{/* Extra padding at bottom to give space for scrolling */}
|
||||
<Box css={{ width: '100%', height: '200px' }} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { VStack } from '../../elements/LayoutPrimitives'
|
||||
import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
|
||||
import { Menubar } from '../Menu'
|
||||
|
||||
export function LibraryMenu(): JSX.Element {
|
||||
useGetUserPreferences()
|
||||
|
||||
return (
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '286px',
|
||||
minWidth: '286px',
|
||||
pl:'15px',
|
||||
height: 'calc(100% - 100px)',
|
||||
overflowY: 'auto',
|
||||
fontWeight: '600',
|
||||
}}
|
||||
>
|
||||
<Menubar />
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Clock, Sliders, X } from 'phosphor-react'
|
||||
import Downshift from 'downshift'
|
||||
|
||||
import { HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { SearchCoordinator } from './LibraryContainer'
|
||||
|
||||
// Styles
|
||||
const List = styled('ul', {
|
||||
width: '91%',
|
||||
maxHeight: '400px',
|
||||
overflow: 'auto',
|
||||
top: '65px',
|
||||
left: '-32px',
|
||||
color: 'var(--colors-utilityTextDefault)',
|
||||
backgroundColor: 'var(--colors-grayBase)',
|
||||
position: 'absolute',
|
||||
zIndex: '2',
|
||||
'@smDown': {
|
||||
fontSize: 16,
|
||||
},
|
||||
})
|
||||
|
||||
const Item = styled('li', {
|
||||
listStyleType: 'none',
|
||||
m: '8px',
|
||||
borderRadius: '5px',
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
export type LibrarySearchBarProps = {
|
||||
coordinator: SearchCoordinator
|
||||
}
|
||||
|
||||
export function LibrarySearchBar(props: LibrarySearchBarProps): JSX.Element {
|
||||
const [recentSearches, setRecentSearches] = useState(Array<unknown[]>())
|
||||
|
||||
useEffect(() => {
|
||||
setRecentSearches(Object.values(localStorage))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Downshift itemToString={(item) => (item ? item : '')}>
|
||||
{({
|
||||
getInputProps,
|
||||
getRootProps,
|
||||
getMenuProps,
|
||||
getItemProps,
|
||||
isOpen,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
clearSelection,
|
||||
openMenu,
|
||||
}) => (
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ pl: '32px', width: '100%', height: '100%' }}
|
||||
>
|
||||
<HStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ width: '100%', borderBottom: 'solid 1px $grayBorder' }}
|
||||
{...getRootProps({ refKey: 'ref' }, { suppressRefError: true })}
|
||||
>
|
||||
<form
|
||||
style={{ width: '100%' }}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
// props.applySearchQuery(searchTerm || '')
|
||||
// inputRef.current?.blur()
|
||||
}}
|
||||
{...getRootProps()}
|
||||
>
|
||||
<FormInput
|
||||
css={{
|
||||
width: '77%',
|
||||
height: '80px',
|
||||
fontSize: '24px',
|
||||
fontFamily: 'Inter',
|
||||
}}
|
||||
type="text"
|
||||
tabIndex={0}
|
||||
value={inputValue}
|
||||
placeholder="Search"
|
||||
onFocus={(event: any) => {
|
||||
event.preventDefault()
|
||||
openMenu()
|
||||
//props.applySearchQuery('')
|
||||
// inputRef.current?.blur()
|
||||
}}
|
||||
onChange={(event: any) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
{...getInputProps()}
|
||||
/>
|
||||
|
||||
{/* {searchTerm && ( */}
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'right',
|
||||
alignItems: 'center',
|
||||
margin: '-57px 0 10px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
clearSelection()
|
||||
//props.applySearchQuery('')
|
||||
// inputRef.current?.blur()
|
||||
}}
|
||||
css={{
|
||||
mr: '15px',
|
||||
}}
|
||||
>
|
||||
<X
|
||||
width={24}
|
||||
height={24}
|
||||
color={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
type="submit"
|
||||
css={{
|
||||
mr: '15px',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
if (inputValue && inputValue.length > 0) {
|
||||
localStorage.setItem(inputValue, inputValue)
|
||||
setRecentSearches(Object.values(localStorage))
|
||||
}
|
||||
// props.applySearchQuery('')
|
||||
// inputRef.current?.blur()
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
{/* )} */}
|
||||
{/* {!searchTerm && ( */}
|
||||
<Button
|
||||
style="plainIcon"
|
||||
onClick={(event) => {
|
||||
// Display the advanced search sheet
|
||||
}}
|
||||
>
|
||||
<Sliders
|
||||
size={24}
|
||||
color={theme.colors.utilityTextDefault.toString()}
|
||||
/>
|
||||
</Button>
|
||||
{/* )} */}
|
||||
</HStack>
|
||||
|
||||
<List>
|
||||
{isOpen &&
|
||||
recentSearches
|
||||
.filter((item) => !inputValue || item.includes(inputValue))
|
||||
.map((item, index) => (
|
||||
<SpanBox
|
||||
{...getMenuProps()}
|
||||
key={item}
|
||||
css={{
|
||||
paddingLeft: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid $grayBorder',
|
||||
'& svg': {
|
||||
margin: '10px',
|
||||
},
|
||||
backgroundColor:
|
||||
index === highlightedIndex
|
||||
? 'var(--colors-grayBg)'
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Clock size={20} />
|
||||
<Item
|
||||
{...getItemProps({
|
||||
item,
|
||||
index,
|
||||
})}
|
||||
>
|
||||
{item}
|
||||
</Item>
|
||||
<X
|
||||
width={20}
|
||||
height={20}
|
||||
color={theme.colors.grayTextContrast.toString()}
|
||||
onClick={() => {
|
||||
localStorage.removeItem(`${item}`)
|
||||
setRecentSearches(Object.values(localStorage))
|
||||
}}
|
||||
/>
|
||||
</SpanBox>
|
||||
))}
|
||||
</List>
|
||||
</form>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)}
|
||||
</Downshift>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { subscribeMutation } from '../../../lib/networking/mutations/subscribeMutation'
|
||||
import { VStack } from '../../elements/LayoutPrimitives'
|
||||
import { OnboardingLayout } from '../OnboardingLayout'
|
||||
import { SelectOption } from './SelectOption'
|
||||
|
||||
const newsletterOptions = [
|
||||
{
|
||||
icon: 'AxiosDaily.png',
|
||||
label: 'Axios Daily Essentials',
|
||||
description: 'Start and end your day with the stories that matter in your inbox.',
|
||||
name: 'axios_essentials',
|
||||
isChecked: false,
|
||||
},
|
||||
{
|
||||
icon: 'MilkRoad.png',
|
||||
label: 'Milk Road',
|
||||
description: '5 minute daily newsletter. Used by 100,000+ people to be better crypto investors 💪',
|
||||
name: 'morning_brew',
|
||||
isChecked: false,
|
||||
},
|
||||
{
|
||||
icon: 'MoneyStuff.png',
|
||||
label: 'Money Stuff by Matt Levine',
|
||||
description: 'A daily take on Wall Street, finance, companies and other stuff.',
|
||||
name: 'milk_road',
|
||||
isChecked: false,
|
||||
},
|
||||
{
|
||||
icon: 'OmnivoreBlog.png',
|
||||
label: 'Omnivore',
|
||||
description: 'Tips and tricks, plus updates on new features in Omnivore.',
|
||||
name: 'omnivore_blog',
|
||||
isChecked: false,
|
||||
},
|
||||
]
|
||||
|
||||
type OnboardingAddNewslettersProps = {
|
||||
pageNumber: number
|
||||
}
|
||||
|
||||
export type NewsLetterOption = {
|
||||
icon: string,
|
||||
label: string,
|
||||
name: string,
|
||||
description: string,
|
||||
isChecked: boolean,
|
||||
}
|
||||
|
||||
export const OnboardingAddNewsletters = (props: OnboardingAddNewslettersProps) => {
|
||||
|
||||
const [newsletters, setNewsletters] = useState<NewsLetterOption[]>(newsletterOptions);
|
||||
|
||||
const onCheck = (index: number) => {
|
||||
const temp = [...newsletters]
|
||||
temp[index].isChecked = !temp[index].isChecked
|
||||
setNewsletters(temp)
|
||||
}
|
||||
|
||||
const onNext = () => {
|
||||
newsletters.map((newsletter) => {
|
||||
if (newsletter.isChecked) subscribeMutation(newsletter.name)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<OnboardingLayout
|
||||
pageNumber={props.pageNumber}
|
||||
subTitle="Omnivore creates an email address for you to subscribe to newsletters with."
|
||||
description="Subscribe to some newsletters now"
|
||||
title="Read all your Newsletters in Omnivore"
|
||||
onNext={onNext}
|
||||
>
|
||||
<VStack css={{
|
||||
marginTop: '$4',
|
||||
width: '100%'
|
||||
}}>
|
||||
{newsletters.map(({ icon, label, description, isChecked }, idx) => (
|
||||
<SelectOption
|
||||
key={idx}
|
||||
indexNum={idx}
|
||||
onCheck={onCheck}
|
||||
{...{ icon, label, description, isChecked}} />
|
||||
))}
|
||||
</VStack>
|
||||
</OnboardingLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import React from 'react'
|
||||
import { OnboardingLayout } from '../OnboardingLayout'
|
||||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { styled } from '../../tokens/stitches.config'
|
||||
|
||||
const StyledImage = styled('img', {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'@smDown': {
|
||||
width: '160%',
|
||||
height: 'auto',
|
||||
}
|
||||
})
|
||||
|
||||
type OnboardingOrganizeInstructionsProps = {
|
||||
pageNumber: number
|
||||
}
|
||||
|
||||
export const OnboardingHighlightInstructions = (props: OnboardingOrganizeInstructionsProps) => {
|
||||
return (
|
||||
<OnboardingLayout
|
||||
pageNumber={props.pageNumber}
|
||||
title="Highlight and share"
|
||||
subTitle="Information about creating and sharing highlights Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint. Velit officia consequat duis enim velit mollit. Exercitation veniam consequat sunt nostrud amet."
|
||||
>
|
||||
<Box css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
'@smDown': {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
}
|
||||
}}>
|
||||
<StyledImage
|
||||
src='/static/images/onboarding/highlight-and-share.svg'
|
||||
alt="Highlight And Share"
|
||||
/>
|
||||
</Box>
|
||||
</OnboardingLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import React from 'react'
|
||||
import { OnboardingLayout } from '../OnboardingLayout'
|
||||
import MobileInstallHelp from '../../elements/MobileInstallHelp'
|
||||
import ExtensionsInstallHelp from '../../elements/ExtensionsInstallHelp'
|
||||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { sendInstallInstructions } from '../../../lib/networking/queries/sendInstallInstructions'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
|
||||
type OnboardingInstallInstructionsProps = {
|
||||
pageNumber: number
|
||||
}
|
||||
|
||||
export const OnboardingInstallInstructions = (props: OnboardingInstallInstructionsProps) => {
|
||||
const onEmailInstructionsClick = async () => {
|
||||
const res = await sendInstallInstructions()
|
||||
if (res !== undefined) {
|
||||
showSuccessToast('Instructions Email Sent', { position: 'bottom-right' })
|
||||
}
|
||||
else {
|
||||
showErrorToast('Failed to send', { position: 'bottom-right' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<OnboardingLayout
|
||||
pageNumber={props.pageNumber}
|
||||
title="Save links to read later"
|
||||
subTitle="Save any link to your library using our apps and browser extensions"
|
||||
description='Install our apps and browser extensions'
|
||||
image={
|
||||
<img
|
||||
src='/static/images/onboarding/browser-extension.svg'
|
||||
alt="Browser Extension"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box css={{
|
||||
"@lgDown": {
|
||||
marginBottom: 100,
|
||||
}
|
||||
}}>
|
||||
<Box
|
||||
css={{
|
||||
margin: '27px 0',
|
||||
'@lg': {
|
||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
boxShadow: '0px 3px 11px rgba(32, 31, 29, 0.04)',
|
||||
},
|
||||
backgroundColor: 'White',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
'@lgDown': {
|
||||
boxShadow: '0px 3px 11px rgba(32, 31, 29, 0.04)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MobileInstallHelp onboarding />
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
'@lgDown': {
|
||||
boxShadow: '0px 3px 11px rgba(32, 31, 29, 0.04)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
marginTop: '8px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ExtensionsInstallHelp onboarding />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
'@lg': {
|
||||
marginBottom: '20px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={onEmailInstructionsClick}
|
||||
css={{
|
||||
fontWeight: '400',
|
||||
fontSize: '14px',
|
||||
lineHeight: '17.5px',
|
||||
color: 'rgba(10, 8, 6, 0.8)',
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Email me instructions
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</OnboardingLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import React from 'react'
|
||||
import { OnboardingLayout } from '../OnboardingLayout'
|
||||
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { styled } from '../../tokens/stitches.config'
|
||||
|
||||
const IconContainer = styled(Box, {width: '30%', justifyContent: 'center', display: 'flex'})
|
||||
const Icon = styled('img', {width: 100})
|
||||
const Row = styled(HStack, {
|
||||
width: '100%',
|
||||
padding: 30,
|
||||
borderBottom: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
|
||||
'@smDown': {
|
||||
padding: 20,
|
||||
}
|
||||
})
|
||||
const Text = styled(Box, {
|
||||
width: '65%',
|
||||
alignSelf: 'center',
|
||||
color: '#0A0806CC',
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
'@smDown': {
|
||||
fontSize: 16,
|
||||
}
|
||||
})
|
||||
const Container = styled(Box, {
|
||||
width: 523,
|
||||
border: '1px solid #0000000F',
|
||||
background: 'white',
|
||||
|
||||
'@smDown': {
|
||||
width: '95%',
|
||||
}
|
||||
})
|
||||
|
||||
type OnboardingJoinCommunityProps = {
|
||||
pageNumber: number
|
||||
}
|
||||
|
||||
export const OnboardingJoinCommunity = (props: OnboardingJoinCommunityProps) => {
|
||||
return (
|
||||
<OnboardingLayout
|
||||
pageNumber={props.pageNumber}
|
||||
title="Join our Community"
|
||||
subTitle='Omnivore is open source and open community, join us.'
|
||||
nextPage='/home'
|
||||
reduceSpace
|
||||
>
|
||||
<HStack distribution='center' alignment='center' css={{width: '100%', height: '100%'}}>
|
||||
<Container>
|
||||
<VStack>
|
||||
<Row distribution='between'>
|
||||
<IconContainer>
|
||||
<Icon css={{height: 60}} src='/static/images/onboarding/github.svg' alt="Github Logo" />
|
||||
</IconContainer>
|
||||
<Text>Star us on Github</Text>
|
||||
</Row>
|
||||
<Row distribution='between'>
|
||||
<IconContainer>
|
||||
<Icon src='/static/images/onboarding/discord.svg' alt="Discrod Logo" />
|
||||
</IconContainer>
|
||||
<Text>Join us on Discord</Text>
|
||||
</Row>
|
||||
<Row distribution='between'>
|
||||
<IconContainer>
|
||||
<Icon css={{width: 74, height: 67}} src='/static/images/onboarding/producthunt.svg' alt="ProductHunt Logo" />
|
||||
</IconContainer>
|
||||
<Text>Like us on Product Hunt</Text>
|
||||
</Row>
|
||||
</VStack>
|
||||
</Container>
|
||||
</HStack>
|
||||
</OnboardingLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import React from 'react'
|
||||
import { OnboardingLayout } from '../OnboardingLayout'
|
||||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { styled } from '../../tokens/stitches.config'
|
||||
|
||||
const StyledImage = styled('img', {
|
||||
position: 'relative',
|
||||
})
|
||||
|
||||
type OnboardingOrganizeInstructionsProps = {
|
||||
pageNumber: number
|
||||
}
|
||||
|
||||
export const OnboardingOrganizeInstructions = (props: OnboardingOrganizeInstructionsProps) => {
|
||||
return (
|
||||
<OnboardingLayout
|
||||
pageNumber={props.pageNumber}
|
||||
title="Organize all your content"
|
||||
subTitle="Information about archiving and tagging
|
||||
Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint. Velit officia consequat duis enim velit mollit. Exercitation veniam consequat sunt nostrud amet."
|
||||
>
|
||||
<Box css={{width: '100%', height: '100%'}}>
|
||||
<StyledImage
|
||||
src='/static/images/onboarding/organize-content.svg'
|
||||
alt="Organize Content Desktop"
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '500px',
|
||||
'@smDown': {
|
||||
display: 'none',
|
||||
},
|
||||
'@lg': {
|
||||
height: '75%',
|
||||
},
|
||||
'@xl': {
|
||||
width: 'auto',
|
||||
height: '95%',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<StyledImage
|
||||
src='/static/images/onboarding/organize-content-mobile.svg'
|
||||
alt="Organize Content Mobile"
|
||||
css={{
|
||||
mt: 10,
|
||||
right: '1.6vh',
|
||||
objectFit: 'contain',
|
||||
width: '100%',
|
||||
'@sm': {
|
||||
display: 'none',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</OnboardingLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import { SelectionOptionCard } from './SelectOption'
|
||||
import { OnboardingLayout } from '../OnboardingLayout'
|
||||
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
|
||||
const articleDetails = [
|
||||
{
|
||||
title: 'Winnebago Electric RV Concept',
|
||||
author: 'Omnivore',
|
||||
originText: 'wired.com',
|
||||
description: "An incredible number of lines from William Shakespeare's plays have becomeso ingrained in modern vernacular …",
|
||||
image: "https://images.hgmsites.net/sml/cadillac_100789665_s.jpg",
|
||||
labels: []
|
||||
},
|
||||
{
|
||||
title: 'Winnebago Electric RV Concept',
|
||||
author: 'Omnivore',
|
||||
originText: 'wired.com',
|
||||
description: "An incredible number of lines from William Shakespeare's plays have becomeso ingrained in modern vernacular …",
|
||||
image: "https://images.hgmsites.net/sml/cadillac_100789665_s.jpg",
|
||||
labels: []
|
||||
},
|
||||
{
|
||||
title: '21 Phrases You Use Without Realizin…',
|
||||
author: 'Omnivore',
|
||||
originText: 'wired.com',
|
||||
description: "An incredible number of lines from William Shakespeare's plays have becomeso ingrained in modern vernacular …",
|
||||
image: "",
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
title: '21 Phrases You Use Without Realizin…',
|
||||
author: 'Omnivore',
|
||||
originText: 'wired.com',
|
||||
description: "An incredible number of lines from William Shakespeare's plays have becomeso ingrained in modern vernacular …",
|
||||
image: "https://images.hgmsites.net/sml/cadillac_100789665_s.jpg",
|
||||
labels: []
|
||||
},
|
||||
]
|
||||
|
||||
type OnboardingReaderPreviewProps = {
|
||||
pageNumber: number
|
||||
}
|
||||
|
||||
export const OnboardingReaderPreview = (props: OnboardingReaderPreviewProps) => {
|
||||
return (
|
||||
<OnboardingLayout
|
||||
pageNumber={props.pageNumber}
|
||||
title="Read Distraction Free"
|
||||
subTitle="Omnivore's distraction free reader gives you an uncluttered reading experience"
|
||||
description='Add some great reads to your library now:'
|
||||
image={
|
||||
<img
|
||||
src='/static/images/onboarding/expanded-controls.svg'
|
||||
alt="Expanded control"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<VStack css={{
|
||||
marginTop: '$4',
|
||||
marginBottom: '$6'
|
||||
}}>
|
||||
<HStack>
|
||||
<Box css={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '100%',
|
||||
gridColumnGap: '$3',
|
||||
gridRowGap: '$2',
|
||||
'@md': {
|
||||
gridTemplateColumns: '50% 50%',
|
||||
},
|
||||
}}>
|
||||
{articleDetails.map(({ title, author, originText, description, image, labels }, idx) => (
|
||||
<SelectionOptionCard
|
||||
key={idx}
|
||||
{...{ title, author, originText, description, image, labels }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</OnboardingLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
import React from 'react'
|
||||
import Checkbox from '../../elements/Checkbox'
|
||||
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { CoverImage } from '../../elements/CoverImage'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { authoredByText } from '../../patterns/ArticleSubtitle'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import Image from 'next/image'
|
||||
|
||||
export const SelectOption: React.FC<{
|
||||
icon: string
|
||||
label: string
|
||||
description: string
|
||||
onCheck: (idx: number) => void
|
||||
indexNum: number
|
||||
isChecked: boolean
|
||||
}> = ({ icon, label, description, onCheck, indexNum, isChecked}) => {
|
||||
|
||||
const toggleChecked = () => {
|
||||
onCheck(indexNum)
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack onClick={toggleChecked} css={{
|
||||
border: isChecked ? '1px solid #F9D354' : '1px solid #0000000F',
|
||||
backgroundColor: isChecked ? '#FDFAEC' : '#FFFFFF',
|
||||
boxShadow: '0px 3px 11px 0px #201F1D0A',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
padding: '$2 $3',
|
||||
marginBottom: 7,
|
||||
width: '100%'
|
||||
}}>
|
||||
<Checkbox checked={isChecked} setChecked={() => undefined} />
|
||||
<SpanBox css={{ marginLeft: '$2', width: '32px', height: '32px' }}>
|
||||
<Image width={32} height={32} layout="fixed"
|
||||
src={`/static/images/newsletter/${icon}`}
|
||||
alt={`${icon.slice(0, -4)} logo`}
|
||||
/>
|
||||
</SpanBox>
|
||||
<VStack css={{ border: '1', marginLeft: '$2', color: '#0A0806CC', }}>
|
||||
<StyledText
|
||||
css={{
|
||||
margin: 0,
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</StyledText>
|
||||
<StyledText
|
||||
css={{
|
||||
margin: 0,
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
color: '#0A0806CC',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</StyledText>
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
export const SelectionOptionCard: React.FC <{
|
||||
title: string,
|
||||
author: string,
|
||||
originText: string,
|
||||
description: string,
|
||||
image: string,
|
||||
labels: Label[]
|
||||
}> = ({title, author, originText, description, image, labels}) => {
|
||||
const [checked, setChecked] = React.useState(false)
|
||||
const toggleChecked = () => setChecked(!checked)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
p: '$2',
|
||||
pr: '8px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'clip',
|
||||
boxShadow: '0px 3px 11px rgba(32, 31, 29, 0.04)',
|
||||
position: 'relative',
|
||||
marginRight: '$3',
|
||||
border: checked ? '1px solid #F9D354' : '1px solid $grayBorder',
|
||||
backgroundColor: checked ? '#FDFAEC' : '#FFFFFF',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onClick={toggleChecked}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
position: 'absolute',
|
||||
top: '1px',
|
||||
left: '1px',
|
||||
width: 'calc(100% - 2px)',
|
||||
'& > div': {
|
||||
borderRadius: '100vmax 100vmax 0 0',
|
||||
},
|
||||
}}
|
||||
>
|
||||
</Box>
|
||||
<VStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{
|
||||
px: '0px',
|
||||
width: '100%',
|
||||
pl: '$1',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
p: '0px',
|
||||
mr: '-12px',
|
||||
mt: '15px',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 24px',
|
||||
gridTemplateRows: '1fr',
|
||||
}}
|
||||
>
|
||||
<HStack css={{
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Checkbox {...{checked, setChecked}} />
|
||||
<HStack css={{marginRight: '$2'}} />
|
||||
<CardTitle title= {title} />
|
||||
</HStack>
|
||||
<Box
|
||||
css={{ alignSelf: 'end', alignItems: 'start', height: '100%' }}
|
||||
onClick={(e) => {
|
||||
// This is here to prevent menu click events from bubbling
|
||||
// up and causing us to "click" on the link item.
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
</Box>
|
||||
</HStack>
|
||||
<HStack alignment="start" distribution="between">
|
||||
<StyledText style="caption" css={{ my: '0' }}>
|
||||
{author && (
|
||||
<SpanBox css={{ mr: '8px' }}>
|
||||
{authoredByText(author)}
|
||||
</SpanBox>
|
||||
)}
|
||||
<SpanBox css={{ textDecorationLine: 'underline' }}>
|
||||
{originText}
|
||||
</SpanBox>
|
||||
</StyledText>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HStack
|
||||
alignment="start"
|
||||
distribution="between"
|
||||
css={{
|
||||
width: '100%',
|
||||
pt: '$2',
|
||||
px: '$1',
|
||||
pr: '12px',
|
||||
mt: '7px',
|
||||
flexGrow: '1',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
m: 0,
|
||||
py: '0px',
|
||||
mr: '$2',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '14px',
|
||||
lineHeight: '125%',
|
||||
color: '$grayTextContrast',
|
||||
flexGrow: '4',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 5,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
data-testid="listDesc"
|
||||
>
|
||||
{description}
|
||||
</StyledText>
|
||||
{image && (
|
||||
<CoverImage
|
||||
src={image}
|
||||
alt="Link Preview Image"
|
||||
width={135}
|
||||
height={90}
|
||||
css={{ ml: '10px', mb: '8px', borderRadius: '3px' }}
|
||||
onError={(e) => {
|
||||
;(e.target as HTMLElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<Box css={{ display: 'block', mt: '8px' }}>
|
||||
{labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</Box>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type CardTitleProps = {
|
||||
title: string
|
||||
}
|
||||
|
||||
function CardTitle(props: CardTitleProps): JSX.Element {
|
||||
return (
|
||||
<StyledText
|
||||
style="listTitle"
|
||||
data-testid="listTitle"
|
||||
css={{
|
||||
mt: '0',
|
||||
mb: '0',
|
||||
fontWeight: '700',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</StyledText>
|
||||
)
|
||||
}
|
||||
116
packages/web/components/templates/reader/ReaderHeader.tsx
Normal file
116
packages/web/components/templates/reader/ReaderHeader.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { DotsThreeOutline, TextAa } from 'phosphor-react'
|
||||
import { PrimaryDropdown } from '../PrimaryDropdown'
|
||||
import { TooltipWrapped } from '../../elements/Tooltip'
|
||||
import { LogoBox } from '../../elements/LogoBox'
|
||||
import { ReactNode } from 'react'
|
||||
import { HEADER_HEIGHT, MOBILE_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
||||
type ReaderHeaderProps = {
|
||||
alwaysDisplayToolbar: boolean
|
||||
showDisplaySettingsModal: (show: boolean) => void
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: 1,
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
height: HEADER_HEIGHT,
|
||||
display: props.alwaysDisplayToolbar ? 'flex' : 'transparent',
|
||||
pt: '35px',
|
||||
borderBottom: props.alwaysDisplayToolbar
|
||||
? '1px solid $thBorderColor'
|
||||
: '1px solid transparent',
|
||||
'@xlgDown': {
|
||||
height: MOBILE_HEADER_HEIGHT,
|
||||
pt: '0px',
|
||||
bg: '$thBackground3',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<LogoBox />
|
||||
<SpanBox
|
||||
css={{
|
||||
width: '100%',
|
||||
px: '25px',
|
||||
'@lg': {
|
||||
display: props.alwaysDisplayToolbar ? 'flex' : 'none',
|
||||
},
|
||||
'@mdDown': { px: '15px' },
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</SpanBox>
|
||||
{!props.alwaysDisplayToolbar && (
|
||||
<SpanBox
|
||||
css={{
|
||||
width: '100%',
|
||||
'@lgDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ControlButtonBox {...props} />
|
||||
</SpanBox>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ControlButtonBox(props: ReaderHeaderProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: '25px',
|
||||
width: '100px',
|
||||
height: '100%',
|
||||
gap: '20px',
|
||||
minWidth: '121px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="articleActionIcon"
|
||||
onClick={() => {
|
||||
props.showDisplaySettingsModal(true)
|
||||
}}
|
||||
>
|
||||
<TooltipWrapped tooltipContent="Adjust Display Settings">
|
||||
<TextAa size={25} color={theme.colors.thHighContrast.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
<PrimaryDropdown showThemeSection={false}>
|
||||
<DotsThreeOutline
|
||||
size={25}
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</PrimaryDropdown>
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue