Merge pull request #394 from omnivore-app/header-and-reader-improvements
Add margins on wider screens, keyboard commands to adjust
This commit is contained in:
@ -115,6 +115,7 @@ export function makeApolloServer(): ApolloServer {
|
||||
schema: schema,
|
||||
context: contextFunc,
|
||||
formatError: (err) => {
|
||||
console.log('server error', err)
|
||||
Sentry.captureException(err)
|
||||
// hide error messages from frontend on prod
|
||||
return new Error('Unexpected server error')
|
||||
|
||||
@ -153,6 +153,14 @@ export const Button = styled('button', {
|
||||
opacity: 0.8,
|
||||
},
|
||||
},
|
||||
articleActionIcon: {
|
||||
bg: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
},
|
||||
ghost: {
|
||||
color: 'transparent',
|
||||
border: 'none',
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
Arrow,
|
||||
Label,
|
||||
} from '@radix-ui/react-dropdown-menu'
|
||||
import { PopperContentProps } from '@radix-ui/react-popover';
|
||||
import { CSS } from '@stitches/react';
|
||||
import { styled } from './../tokens/stitches.config'
|
||||
|
||||
@ -49,12 +50,12 @@ export const DropdownContent = styled(Content, {
|
||||
backgroundColor: '$grayBg',
|
||||
borderRadius: '0.5em',
|
||||
padding: 5,
|
||||
border: '1px solid $grayBorder',
|
||||
outline: '1px solid $grayBorder',
|
||||
boxShadow: '$cardBoxShadow',
|
||||
})
|
||||
|
||||
const StyledArrow = styled(Arrow, {
|
||||
fill: '$grayBase',
|
||||
fill: '$grayBg',
|
||||
})
|
||||
|
||||
const StyledLabel = styled(Label, {
|
||||
@ -65,6 +66,7 @@ const StyledLabel = styled(Label, {
|
||||
})
|
||||
|
||||
export type DropdownAlignment = 'start' | 'end' | 'center'
|
||||
export type DropdownSide = 'top' | 'right' | 'bottom' | 'left'
|
||||
|
||||
type DropdownProps = {
|
||||
labelText?: string
|
||||
@ -73,8 +75,11 @@ type DropdownProps = {
|
||||
children: React.ReactNode
|
||||
styledArrow?: boolean
|
||||
align?: DropdownAlignment
|
||||
side?: DropdownSide
|
||||
sideOffset?: number
|
||||
disabled?: boolean
|
||||
css?: CSS
|
||||
modal?: boolean
|
||||
}
|
||||
|
||||
export const DropdownSeparator = styled(Separator, {
|
||||
@ -101,17 +106,22 @@ export function DropdownOption(props: DropdownOptionProps): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
export function Dropdown({
|
||||
children,
|
||||
align,
|
||||
triggerElement,
|
||||
labelText,
|
||||
showArrow = true,
|
||||
disabled = false,
|
||||
css
|
||||
}: DropdownProps): JSX.Element {
|
||||
export function Dropdown(props: DropdownProps & PopperContentProps): JSX.Element {
|
||||
const {
|
||||
children,
|
||||
align,
|
||||
triggerElement,
|
||||
labelText,
|
||||
showArrow = true,
|
||||
disabled = false,
|
||||
side = 'bottom',
|
||||
sideOffset = 0,
|
||||
alignOffset = 0,
|
||||
css,
|
||||
modal
|
||||
} = props
|
||||
return (
|
||||
<Root modal={false}>
|
||||
<Root modal={modal}>
|
||||
<DropdownTrigger disabled={disabled}>{triggerElement}</DropdownTrigger>
|
||||
<DropdownContent
|
||||
css={css}
|
||||
@ -119,11 +129,14 @@ export function Dropdown({
|
||||
// remove focus from dropdown
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align ? align : 'center'}
|
||||
alignOffset={alignOffset}
|
||||
>
|
||||
{labelText && <StyledLabel>{labelText}</StyledLabel>}
|
||||
{children}
|
||||
{showArrow && <StyledArrow offset={20} />}
|
||||
{showArrow && <StyledArrow offset={20} width={20} height={10} />}
|
||||
</DropdownContent>
|
||||
</Root>
|
||||
)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { SpanBox } from './LayoutPrimitives'
|
||||
import { StyledText } from './StyledText'
|
||||
|
||||
type LabelChipProps = {
|
||||
@ -16,19 +18,25 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
|
||||
}
|
||||
const color = hexToRgb(props.color)
|
||||
return (
|
||||
<StyledText
|
||||
css={{
|
||||
margin: '4px',
|
||||
borderRadius: '32px',
|
||||
color: props.color,
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
padding: '4px 8px 4px 8px',
|
||||
border: `1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7)`,
|
||||
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.08)`,
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
</StyledText>
|
||||
<Link href={`/home?q=label:"${props.text}"`}>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'inline-table',
|
||||
margin: '4px',
|
||||
borderRadius: '32px',
|
||||
color: props.color,
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
padding: '4px 8px 4px 8px',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer',
|
||||
backgroundClip: 'padding-box',
|
||||
border: `1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7)`,
|
||||
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.08)`,
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
</SpanBox>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,21 +2,18 @@ import React, { useState } from 'react'
|
||||
import { styled } from '../tokens/stitches.config'
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
import { HexColorPicker } from 'react-colorful'
|
||||
import { Button } from './Button'
|
||||
import { HStack, SpanBox } from './LayoutPrimitives'
|
||||
import { CaretDown } from 'phosphor-react'
|
||||
import { StyledText } from './StyledText'
|
||||
import {
|
||||
ColorDetailsProps,
|
||||
LabelColor,
|
||||
LabelColorDropdownProps,
|
||||
LabelColorHex,
|
||||
LabelColorObject,
|
||||
LabelOptionProps,
|
||||
} from '../../utils/settings-page/labels/types'
|
||||
import { labelColorObjects } from '../../utils/settings-page/labels/labelColorObjects'
|
||||
import { DropdownOption } from './DropdownElements'
|
||||
import { isDarkTheme } from '../../lib/themeUpdater'
|
||||
import { LabelColor } from '../../lib/networking/fragments/labelFragment'
|
||||
|
||||
const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, {
|
||||
maxWidth: 190,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Root, Overlay, Content } from '@radix-ui/react-dialog'
|
||||
import { styled, keyframes } from '../tokens/stitches.config'
|
||||
import { styled, keyframes, theme } from '../tokens/stitches.config'
|
||||
|
||||
export const ModalRoot = styled(Root, {})
|
||||
|
||||
@ -25,8 +25,7 @@ const contentShow = keyframes({
|
||||
const Modal = styled(Content, {
|
||||
backgroundColor: '$grayBg',
|
||||
borderRadius: 6,
|
||||
boxShadow:
|
||||
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
boxShadow: theme.shadows.cardBoxShadow.toString(),
|
||||
position: 'fixed',
|
||||
'&:focus': { outline: 'none' },
|
||||
})
|
||||
|
||||
39
packages/web/components/elements/TickedRangeSlider.tsx
Normal file
39
packages/web/components/elements/TickedRangeSlider.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
|
||||
|
||||
import { Box, HStack } from './LayoutPrimitives'
|
||||
import { styled, theme } from '../tokens/stitches.config'
|
||||
|
||||
type TickedRangeSliderProps = {
|
||||
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,
|
||||
})
|
||||
|
||||
export function TickedRangeSlider({
|
||||
ticks = 8,
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -62,6 +62,15 @@ type TooltipWrappedProps = {
|
||||
style?: TooltipPrimitive.TooltipContentProps['style']
|
||||
}
|
||||
|
||||
const DefaultTooltipStyle = {
|
||||
backgroundColor: '#F9D354',
|
||||
color: '#0A0806',
|
||||
}
|
||||
|
||||
const DefaultArrowStyle = {
|
||||
fill: '#F9D354'
|
||||
}
|
||||
|
||||
export const TooltipWrapped: FC<TooltipWrappedProps> = ({
|
||||
children,
|
||||
active,
|
||||
@ -73,9 +82,14 @@ export const TooltipWrapped: FC<TooltipWrappedProps> = ({
|
||||
return (
|
||||
<Tooltip open={active}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent sideOffset={5} side={tooltipSide} {...props}>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
side={tooltipSide}
|
||||
style={DefaultTooltipStyle}
|
||||
{...props}
|
||||
>
|
||||
{tooltipContent}
|
||||
<TooltipArrow style={arrowStyles} />
|
||||
<TooltipArrow style={arrowStyles ?? DefaultArrowStyle} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
14
packages/web/components/elements/images/AIcon.tsx
Normal file
14
packages/web/components/elements/images/AIcon.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
type AIconProps = {
|
||||
size: number
|
||||
color: string
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export function AIcon(props: AIconProps): JSX.Element {
|
||||
return (
|
||||
<svg style={props.style} width={props.size} height={props.size} viewBox={`0 0 20 20`} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.5 18L11.75 5.25L5 18" stroke={props.color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M16.5133 14.25H6.98828" stroke={props.color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { config } from '../../tokens/stitches.config'
|
||||
import Image from 'next/image'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import Link from 'next/link'
|
||||
import { SpanBox } from '../LayoutPrimitives'
|
||||
|
||||
export function OmnivoreNameLogoImage(): JSX.Element {
|
||||
return (
|
||||
@ -51,7 +52,7 @@ export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element {
|
||||
<Link passHref href={href}>
|
||||
<a style={{ textDecoration: 'none', display: 'flex', alignItems: 'center' }}>
|
||||
<OmnivoreLogoIcon size={27} strokeColor={fillColor}></OmnivoreLogoIcon>
|
||||
<StyledText style="logoTitle" css={{ color: fillColor, paddingLeft: '12px' }}>Omnivore</StyledText>
|
||||
{/* <StyledText style="logoTitle" css={{ color: fillColor, paddingLeft: '12px' }}>Omnivore</StyledText> */}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@ -16,6 +16,7 @@ export type HeaderDropdownAction =
|
||||
| 'apply-lighter-theme'
|
||||
| 'navigate-to-install'
|
||||
| 'navigate-to-emails'
|
||||
| 'navigate-to-labels'
|
||||
| 'navigate-to-profile'
|
||||
| 'increaseFontSize'
|
||||
| 'decreaseFontSize'
|
||||
@ -24,7 +25,6 @@ export type HeaderDropdownAction =
|
||||
type DropdownMenuProps = {
|
||||
username?: string
|
||||
triggerElement: ReactNode
|
||||
displayFontStepper?: boolean
|
||||
actionHandler: (action: HeaderDropdownAction) => void
|
||||
}
|
||||
|
||||
@ -53,24 +53,6 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
|
||||
{ isDark ? '✓' : '' }
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{props.displayFontStepper && (
|
||||
<>
|
||||
<HStack css={{ mt: '8px', width: '100%', height: '26px', gap: '8px', borderRadius: '6px', border: '1px solid $grayTextContrast', }}>
|
||||
<Button style='plainIcon' css={{ display: 'inline-block', verticalAlign: 'baseline', width: '50%', height: '100%', bg: 'unset' }} onClick={() => {
|
||||
props.actionHandler('decreaseFontSize')
|
||||
}}>
|
||||
<StyledText css={{ fontSize: '14px', m: '0px' }}>A</StyledText>
|
||||
</Button>
|
||||
<Box css={{ width: '1px', height: '100%', bg: '$grayTextContrast' }} />
|
||||
<Button style='plainIcon' css={{ display: 'inline-block', verticalAlign: 'baseline', width: '50%', height: '100%', bg: 'unset' }} onClick={() => {
|
||||
props.actionHandler('increaseFontSize')
|
||||
}}>
|
||||
<StyledText css={{ fontSize: '18px', m: '0px' }}>A</StyledText>
|
||||
</Button>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
<DropdownSeparator />
|
||||
<DropdownOption
|
||||
@ -81,6 +63,10 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
|
||||
onSelect={() => props.actionHandler('navigate-to-emails')}
|
||||
title="Emails"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('navigate-to-labels')}
|
||||
title="Labels"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => window.Intercom('show')}
|
||||
title="Feedback"
|
||||
|
||||
@ -44,7 +44,7 @@ export function HighlightBar(props: HighlightBarProps): JSX.Element {
|
||||
background: '$grayBg',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid $grayBorder',
|
||||
boxShadow: '0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)',
|
||||
boxShadow: theme.shadows.cardBoxShadow.toString(),
|
||||
bottom: 'calc(38px + env(safe-area-inset-bottom, 40px))',
|
||||
'@smDown': {
|
||||
maxWidth: '80%',
|
||||
|
||||
@ -154,11 +154,11 @@ export function GridLinkedItemCard(props: LinkedItemCardProps): JSX.Element {
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack css={{ mt: '8px' }}>
|
||||
{props.item.labels?.map(({ description, color }, index) => (
|
||||
<LabelChip key={index} text={description || ''} color={color} />
|
||||
{/* <HStack css={{ mt: '8px' }}>
|
||||
{props.item.labels?.map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</HStack>
|
||||
</HStack> */}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@ import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { setupAnalytics } from '../../lib/analytics'
|
||||
import { Button } from '../elements/Button'
|
||||
import Link from 'next/link'
|
||||
import { ArrowSquareOut } from 'phosphor-react'
|
||||
|
||||
type HeaderProps = {
|
||||
user?: UserBasicData
|
||||
@ -26,7 +25,7 @@ type HeaderProps = {
|
||||
profileImageURL?: string
|
||||
isFixedPosition: boolean
|
||||
scrollElementRef?: React.RefObject<HTMLDivElement>
|
||||
displayFontStepper?: boolean
|
||||
toolbarControl?: JSX.Element
|
||||
setShowLogoutConfirmation: (showShareModal: boolean) => void
|
||||
setShowKeyboardCommandsModal: (showShareModal: boolean) => void
|
||||
}
|
||||
@ -55,9 +54,6 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
|
||||
(changeset: ScrollOffsetChangeset) => {
|
||||
const isScrolledBeyondMinThreshold = changeset.current.y >= 50
|
||||
const isScrollingDown = changeset.current.y > changeset.previous.y
|
||||
|
||||
// setIsScrolled(isScrolledBeyondMinThreshold)
|
||||
// setShowHeader(!(isScrollingDown && isScrolledBeyondMinThreshold))
|
||||
},
|
||||
0
|
||||
)
|
||||
@ -106,6 +102,9 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
|
||||
case 'navigate-to-emails':
|
||||
router.push('/settings/emails')
|
||||
break
|
||||
case 'navigate-to-labels':
|
||||
router.push('/settings/labels')
|
||||
break
|
||||
case 'navigate-to-profile':
|
||||
if (props.user) {
|
||||
router.push(`/${props.user.profile.username}`)
|
||||
@ -128,7 +127,7 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
|
||||
isDisplayingShadow={isScrolled}
|
||||
isVisible={true}
|
||||
isFixedPosition={true}
|
||||
displayFontStepper={props.displayFontStepper}
|
||||
toolbarControl={props.toolbarControl}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@ -143,7 +142,7 @@ type NavHeaderProps = {
|
||||
isDisplayingShadow?: boolean
|
||||
isVisible?: boolean
|
||||
isFixedPosition: boolean
|
||||
displayFontStepper?: boolean
|
||||
toolbarControl?: JSX.Element
|
||||
}
|
||||
|
||||
function NavHeader(props: NavHeaderProps): JSX.Element {
|
||||
@ -159,16 +158,16 @@ function NavHeader(props: NavHeaderProps): JSX.Element {
|
||||
zIndex: 5,
|
||||
width: '100%',
|
||||
boxShadow: props.isDisplayingShadow ? '$panelShadow' : 'unset',
|
||||
bg: '$grayBase',
|
||||
p: '0px $3 0px $3',
|
||||
height: '68px',
|
||||
height: '48px',
|
||||
position: 'fixed',
|
||||
minHeight: '68px',
|
||||
bg: 'transparent',
|
||||
'@smDown': {
|
||||
height: '48px',
|
||||
minHeight: '48px',
|
||||
p: '0px 18px 0px 16px',
|
||||
},
|
||||
'@lgDown': {
|
||||
bg: '$grayBase',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HStack alignment="center" distribution="start">
|
||||
@ -183,26 +182,26 @@ function NavHeader(props: NavHeaderProps): JSX.Element {
|
||||
</Box>
|
||||
<NavLinks currentPath={currentPath} isLoggedIn={!!props.username} />
|
||||
</HStack>
|
||||
|
||||
{props.toolbarControl && (
|
||||
<HStack distribution="end" alignment="center" css={{
|
||||
height: '100%', width: '100%',
|
||||
mr: '16px',
|
||||
display: 'none',
|
||||
'@lgDown': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.toolbarControl}
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{props.username ? (
|
||||
<HStack
|
||||
alignment="center"
|
||||
css={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Box css={{ '@smDown': { visibility: 'collapse' } }}>
|
||||
<a href="https://github.com/omnivore-app/omnivore" target='_blank' rel="noreferrer">
|
||||
<Button style="ctaLightGray" css={{ background: 'unset', mr: '32px' }}>
|
||||
<HStack css={{ height: '100%' }}>
|
||||
<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill={theme.colors.grayTextContrast.toString()} fillRule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
<SpanBox css={{ pl: '8px', color: '$grayTextContrast' }}>Follow us on GitHub</SpanBox>
|
||||
<Box className='ctaButtonIcon' css={{ ml: '4px' }}>
|
||||
<ArrowSquareOut size={16} />
|
||||
</Box>
|
||||
</HStack>
|
||||
</Button>
|
||||
</a>
|
||||
</Box>
|
||||
<DropdownMenu
|
||||
username={props.username}
|
||||
triggerElement={
|
||||
@ -212,7 +211,6 @@ function NavHeader(props: NavHeaderProps): JSX.Element {
|
||||
/>
|
||||
}
|
||||
actionHandler={props.actionHandler}
|
||||
displayFontStepper={props.displayFontStepper}
|
||||
/>
|
||||
</HStack>
|
||||
) : (
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
ReactNode,
|
||||
MutableRefObject,
|
||||
useEffect,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { PrimaryHeader } from './../patterns/PrimaryHeader'
|
||||
@ -23,7 +22,7 @@ type PrimaryLayoutProps = {
|
||||
hideHeader?: boolean
|
||||
pageMetaDataProps?: PageMetaDataProps
|
||||
scrollElementRef?: MutableRefObject<HTMLDivElement | null>
|
||||
displayFontStepper?: boolean
|
||||
headerToolbarControl?: JSX.Element
|
||||
}
|
||||
|
||||
export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
||||
@ -62,23 +61,28 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
||||
{props.pageMetaDataProps ? (
|
||||
<PageMetaData {...props.pageMetaDataProps} />
|
||||
) : null}
|
||||
<Box css={{ bg: '$grayBase', height: '100vh', width: '100vw' }}>
|
||||
<Box css={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
bg: 'transparent',
|
||||
'@smDown': {
|
||||
bg: '$grayBase',
|
||||
}
|
||||
}}>
|
||||
<PrimaryHeader
|
||||
user={viewerData?.me}
|
||||
hideHeader={props.hideHeader}
|
||||
userInitials={viewerData?.me?.name.charAt(0) ?? ''}
|
||||
profileImageURL={viewerData?.me?.profile.pictureUrl}
|
||||
isFixedPosition={true}
|
||||
toolbarControl={props.headerToolbarControl}
|
||||
scrollElementRef={props.scrollElementRef}
|
||||
displayFontStepper={props.displayFontStepper}
|
||||
setShowLogoutConfirmation={setShowLogoutConfirmation}
|
||||
setShowKeyboardCommandsModal={setShowKeyboardCommandsModal}
|
||||
/>
|
||||
<Box
|
||||
ref={props.scrollElementRef}
|
||||
css={{
|
||||
top: '68px',
|
||||
'@smDown': { top: '48px' },
|
||||
position: 'fixed',
|
||||
overflowY: 'auto',
|
||||
height: '100%',
|
||||
@ -86,6 +90,13 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={props.scrollElementRef}
|
||||
css={{
|
||||
height: '48px',
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
></Box>
|
||||
{props.children}
|
||||
{showLogoutConfirmation ? (
|
||||
<ConfirmationModal
|
||||
|
||||
@ -54,8 +54,7 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
|
||||
/>
|
||||
<Box
|
||||
css={{
|
||||
top: '68px',
|
||||
'@smDown': { top: '48px' },
|
||||
top: '48px',
|
||||
position: 'fixed',
|
||||
overflowY: 'auto',
|
||||
height: '100%',
|
||||
|
||||
154
packages/web/components/templates/article/ArticleActionsMenu.tsx
Normal file
154
packages/web/components/templates/article/ArticleActionsMenu.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import { Separator } from "@radix-ui/react-separator"
|
||||
import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react"
|
||||
import { ArticleAttributes } from "../../../lib/networking/queries/useGetArticleQuery"
|
||||
import { useGetUserPreferences } from "../../../lib/networking/queries/useGetUserPreferences"
|
||||
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 { ReaderSettingsControl } from "./ReaderSettingsControl"
|
||||
import { usePersistedState } from "../../../lib/hooks/usePersistedState"
|
||||
|
||||
export type ArticleActionsMenuLayout = 'horizontal' | 'vertical'
|
||||
|
||||
type ArticleActionsMenuProps = {
|
||||
article: ArticleAttributes
|
||||
layout: ArticleActionsMenuLayout
|
||||
lineHeight: number
|
||||
marginWidth: number
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
}
|
||||
|
||||
type MenuSeparatorProps = {
|
||||
layout: ArticleActionsMenuLayout
|
||||
}
|
||||
|
||||
const MenuSeparator = (props: MenuSeparatorProps): JSX.Element => {
|
||||
const LineSeparator = styled(Separator, {
|
||||
width: '100%',
|
||||
margin: 0,
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
my: '8px',
|
||||
})
|
||||
return (props.layout == 'vertical' ? <LineSeparator /> : <></>)
|
||||
}
|
||||
|
||||
type ActionDropdownProps = {
|
||||
layout: ArticleActionsMenuLayout
|
||||
triggerElement: JSX.Element
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
const ActionDropdown = (props: ActionDropdownProps): JSX.Element => {
|
||||
return <Dropdown
|
||||
showArrow={true}
|
||||
css={{ m: '0px', p: '0px', overflow: 'hidden', width: '265px', maxWidth: '265px', '@smDown': { width: '230px' } }}
|
||||
side={props.layout == 'vertical' ? 'right' : 'bottom'}
|
||||
sideOffset={props.layout == 'vertical' ? 8 : 0}
|
||||
align={props.layout == 'vertical' ? 'start' : 'center'}
|
||||
alignOffset={props.layout == 'vertical' ? -18 : undefined}
|
||||
triggerElement={props.triggerElement}
|
||||
>
|
||||
{props.children}
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: props.layout == 'vertical' ? 'column' : 'row',
|
||||
justifyContent: props.layout == 'vertical' ? 'center' : 'flex-end',
|
||||
gap: props.layout == 'vertical' ? '8px' : '24px',
|
||||
paddingTop: '6px',
|
||||
}}
|
||||
>
|
||||
|
||||
<ActionDropdown
|
||||
layout={props.layout}
|
||||
triggerElement={
|
||||
<TooltipWrapped
|
||||
tooltipContent="Adjust Display Settings"
|
||||
tooltipSide={props.layout == 'vertical' ? 'right' : 'bottom'}
|
||||
>
|
||||
<TextAa size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
}
|
||||
>
|
||||
<ReaderSettingsControl
|
||||
lineHeight={props.lineHeight}
|
||||
marginWidth={props.marginWidth}
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
/>
|
||||
</ActionDropdown>
|
||||
|
||||
<MenuSeparator layout={props.layout} />
|
||||
|
||||
<SpanBox css={{
|
||||
'display': 'flex',
|
||||
'@smDown': {
|
||||
display: 'none',
|
||||
}}}
|
||||
>
|
||||
<ActionDropdown
|
||||
layout={props.layout}
|
||||
triggerElement={
|
||||
<TooltipWrapped
|
||||
tooltipContent="Edit labels"
|
||||
tooltipSide={props.layout == 'vertical' ? 'right' : 'bottom'}
|
||||
>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
}
|
||||
>
|
||||
<SetLabelsControl
|
||||
article={props.article}
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
/>
|
||||
</ActionDropdown>
|
||||
</SpanBox>
|
||||
|
||||
<Button style='articleActionIcon'
|
||||
onClick={() => props.articleActionHandler('setLabels')}
|
||||
css={{
|
||||
'display': 'none',
|
||||
'@smDown': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
|
||||
<Button style='articleActionIcon' onClick={() => props.articleActionHandler('showHighlights')}>
|
||||
<TooltipWrapped
|
||||
tooltipContent="View Highlights"
|
||||
tooltipSide={props.layout == 'vertical' ? 'right' : 'bottom'}
|
||||
>
|
||||
<HighlighterCircle size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
|
||||
<MenuSeparator layout={props.layout} />
|
||||
|
||||
<Button style='articleActionIcon' onClick={() => props.articleActionHandler('archive')}>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Archive"
|
||||
tooltipSide={props.layout == 'vertical' ? 'right' : 'bottom'}
|
||||
>
|
||||
<ArchiveBox size={24} color={theme.colors.readerFont.toString()} />
|
||||
</TooltipWrapped>
|
||||
</Button>
|
||||
{/* <MenuSeparator layout={props.layout} />
|
||||
|
||||
<Button style='articleActionIcon'>
|
||||
<DotsThree size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button> */}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Article } from './../../../components/templates/article/Article'
|
||||
import { Box, VStack } from './../../elements/LayoutPrimitives'
|
||||
import { Box, SpanBox, VStack } from './../../elements/LayoutPrimitives'
|
||||
import { StyledText } from './../../elements/StyledText'
|
||||
import { ArticleSubtitle } from './../../patterns/ArticleSubtitle'
|
||||
import { theme, ThemeId } from './../../tokens/stitches.config'
|
||||
@ -13,9 +13,12 @@ import { ArticleHeaderToolbar } from './ArticleHeaderToolbar'
|
||||
import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation'
|
||||
import { updateThemeLocally } from '../../../lib/themeUpdater'
|
||||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
|
||||
type ArticleContainerProps = {
|
||||
article: ArticleAttributes
|
||||
labels: Label[]
|
||||
articleMutations: ArticleMutations
|
||||
scrollElementRef: MutableRefObject<HTMLDivElement | null>
|
||||
isAppleAppEmbed: boolean
|
||||
@ -24,21 +27,22 @@ type ArticleContainerProps = {
|
||||
margin?: number
|
||||
fontSize?: number
|
||||
fontFamily?: string
|
||||
lineHeight?: number
|
||||
showHighlightsModal: boolean
|
||||
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
const [showLabelsModal, setShowLabelsModal] = useState(false)
|
||||
const [showNotesSidebar, setShowNotesSidebar] = useState(false)
|
||||
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
|
||||
const [showHighlightsModal, setShowHighlightsModal] = useState(props.showHighlightsModal)
|
||||
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
|
||||
const [labels, setLabels] = useState(
|
||||
props.article.labels?.map((l) => l.id) || []
|
||||
)
|
||||
|
||||
const updateFontSize = async (newFontSize: number) => {
|
||||
setFontSize(newFontSize)
|
||||
await userPersonalizationMutation({ fontSize: newFontSize })
|
||||
if (fontSize !== newFontSize) {
|
||||
setFontSize(newFontSize)
|
||||
await userPersonalizationMutation({ fontSize: newFontSize })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -85,8 +89,9 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
}, [props.article])
|
||||
|
||||
const styles = {
|
||||
margin: props.margin ?? 140,
|
||||
fontSize,
|
||||
margin: props.margin ?? 360,
|
||||
lineHeight: props.lineHeight ?? 150,
|
||||
fontFamily: props.fontFamily ?? 'inter',
|
||||
readerFontColor: theme.colors.readerFont.toString(),
|
||||
readerFontColorTransparent: theme.colors.readerFontTransparent.toString(),
|
||||
@ -100,10 +105,11 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
id="article-container"
|
||||
css={{
|
||||
padding: '16px',
|
||||
maxWidth: '94%',
|
||||
maxWidth: '100%',
|
||||
background: props.isAppleAppEmbed ? 'unset' : theme.colors.grayBg.toString(),
|
||||
'--text-font-family': styles.fontFamily,
|
||||
'--text-font-size': `${styles.fontSize}px`,
|
||||
'--line-height': `150%`,
|
||||
'--line-height': `${styles.lineHeight}%`,
|
||||
'--blockquote-padding': '0.5em 1em',
|
||||
'--blockquote-icon-font-size': '1.3rem',
|
||||
'--figure-margin': '1.6rem auto',
|
||||
@ -117,15 +123,15 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
'--blockquote-icon-font-size': '1.7rem',
|
||||
'--figure-margin': '2.6875rem auto',
|
||||
'--hr-margin': '2em',
|
||||
margin: `30px ${styles.margin / 2}px`,
|
||||
margin: `30px 0px`,
|
||||
},
|
||||
'@md': {
|
||||
maxWidth: '92%',
|
||||
maxWidth: 1024 - (styles.margin),
|
||||
},
|
||||
'@lg': {
|
||||
margin: `30px 0`,
|
||||
width: 'auto',
|
||||
maxWidth: 1024 - styles.margin,
|
||||
maxWidth: 1024 - (styles.margin),
|
||||
},
|
||||
}}
|
||||
>
|
||||
@ -143,13 +149,22 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
author={props.article.author}
|
||||
href={props.article.url}
|
||||
/>
|
||||
<ArticleHeaderToolbar
|
||||
articleTitle={props.article.title}
|
||||
articleShareURL={props.highlightsBaseURL}
|
||||
setShowNotesSidebar={setShowNotesSidebar}
|
||||
setShowShareArticleModal={setShowShareModal}
|
||||
hasHighlights={props.article.highlights?.length > 0}
|
||||
/>
|
||||
{props.labels ? (
|
||||
<SpanBox css={{ pb: '16px', width: '100%', '&:empty': { display: 'none' } }}>
|
||||
{props.labels?.map((label) =>
|
||||
<LabelChip key={label.id} text={label.name} color={label.color} />
|
||||
)}
|
||||
</SpanBox>
|
||||
) : null}
|
||||
{props.isAppleAppEmbed && (
|
||||
<ArticleHeaderToolbar
|
||||
articleTitle={props.article.title}
|
||||
articleShareURL={props.highlightsBaseURL}
|
||||
setShowShareArticleModal={setShowShareModal}
|
||||
setShowHighlightsModal={props.setShowHighlightsModal}
|
||||
hasHighlights={props.article.highlights?.length > 0}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
<Article
|
||||
articleId={props.article.id}
|
||||
@ -181,10 +196,10 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
articleAuthor={props.article.author ?? ''}
|
||||
articleId={props.article.id}
|
||||
isAppleAppEmbed={props.isAppleAppEmbed}
|
||||
highlightBarDisabled={props.highlightBarDisabled}
|
||||
showNotesSidebar={showNotesSidebar}
|
||||
highlightsBaseURL={props.highlightsBaseURL}
|
||||
setShowNotesSidebar={setShowNotesSidebar}
|
||||
highlightBarDisabled={props.highlightBarDisabled}
|
||||
showHighlightsModal={props.showHighlightsModal}
|
||||
setShowHighlightsModal={props.setShowHighlightsModal}
|
||||
articleMutations={props.articleMutations}
|
||||
/>
|
||||
{showReportIssuesModal ? (
|
||||
|
||||
@ -14,7 +14,7 @@ type ArticleHeaderToolbarProps = {
|
||||
articleTitle: string
|
||||
articleShareURL: string
|
||||
hasHighlights: boolean
|
||||
setShowNotesSidebar: (showNotesSidebar: boolean) => void
|
||||
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
setShowShareArticleModal: (showShareModal: boolean) => void
|
||||
}
|
||||
|
||||
@ -46,7 +46,12 @@ export function ArticleHeaderToolbar(
|
||||
return (
|
||||
<HStack distribution="between" alignment="center" css={{ gap: '$2' }}>
|
||||
{props.hasHighlights && (
|
||||
<Button style="plainIcon" onClick={() => props.setShowNotesSidebar(true)} title="View all your highlights and notes">
|
||||
<Button style="plainIcon" onClick={() => {
|
||||
if (props.setShowHighlightsModal) {
|
||||
props.setShowHighlightsModal(true)
|
||||
}
|
||||
}}
|
||||
title="View all your highlights and notes">
|
||||
<CommentIcon
|
||||
size={24}
|
||||
strokeColor={theme.colors.grayTextContrast.toString()}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import { X } from 'phosphor-react'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { UserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import {
|
||||
ModalRoot,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { ReaderSettingsControl } from './ReaderSettingsControl'
|
||||
|
||||
type DisplaySettingsModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
lineHeight: number
|
||||
marginWidth: number
|
||||
articleActionHandler: (action: string, arg?: number) => void
|
||||
}
|
||||
|
||||
export function DisplaySettingsModal(props: DisplaySettingsModalProps): JSX.Element {
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
css={{ overflow: 'auto' }}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<VStack css={{ width: '100%' }}>
|
||||
<HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<StyledText style="modalHeadline" css={{ pl: '16px' }}>Labels</StyledText>
|
||||
<Button
|
||||
css={{ pt: '16px', pr: '16px' }}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<CrossIcon
|
||||
size={14}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
<ReaderSettingsControl
|
||||
lineHeight={props.lineHeight}
|
||||
marginWidth={props.marginWidth}
|
||||
articleActionHandler={props.articleActionHandler}
|
||||
/>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
import {
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
ModalRoot,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { HStack, VStack } 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 { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { ChangeEvent, useCallback, useState } from 'react'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
|
||||
type EditLabelsModalProps = {
|
||||
labels: Label[]
|
||||
article: ArticleAttributes
|
||||
onOpenChange: (open: boolean) => void
|
||||
setLabels: (labels: Label[]) => void
|
||||
}
|
||||
|
||||
export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element {
|
||||
const [selectedLabels, setSelectedLabels] = useState(props.labels)
|
||||
const { labels } = useGetLabelsQuery()
|
||||
|
||||
const saveAndExit = useCallback(async () => {
|
||||
const result = await setLabelsMutation(props.article.id, selectedLabels.map((l) => l.id))
|
||||
console.log('result of setting labels', result)
|
||||
props.onOpenChange(false)
|
||||
props.setLabels(selectedLabels)
|
||||
}, [props, selectedLabels])
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
// const label = event.target.value
|
||||
// if (event.target.checked) {
|
||||
// setSelectedLabels([...selectedLabels, label])
|
||||
// } else {
|
||||
// setSelectedLabels(selectedLabels.filter((l) => l !== label))
|
||||
// }
|
||||
},
|
||||
[selectedLabels]
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={saveAndExit}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
css={{ overflow: 'auto', p: '0' }}
|
||||
>
|
||||
<VStack distribution="start" css={{ p: '0' }}>
|
||||
<HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<StyledText style="modalHeadline" css={{ p: '16px' }}>
|
||||
Edit Labels
|
||||
</StyledText>
|
||||
<Button
|
||||
css={{ pt: '16px', pr: '16px' }}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<CrossIcon
|
||||
size={20}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
{labels &&
|
||||
labels.map((label) => (
|
||||
<HStack
|
||||
key={label.id}
|
||||
css={{ height: '50px', verticalAlign: 'middle' }}
|
||||
onClick={() => {
|
||||
// if (selectedLabels.includes(label.id)) {
|
||||
// setSelectedLabels(
|
||||
// selectedLabels.filter((id) => id !== label.id)
|
||||
// )
|
||||
// } else {
|
||||
// setSelectedLabels([...selectedLabels, label.id])
|
||||
// }
|
||||
}}
|
||||
>
|
||||
<LabelChip color={label.color} text={label.name} />
|
||||
<input
|
||||
type="checkbox"
|
||||
value={label.id}
|
||||
onChange={handleChange}
|
||||
checked={selectedLabels.includes(label)}
|
||||
/>
|
||||
</HStack>
|
||||
))}
|
||||
<HStack css={{ width: '100%', mb: '16px' }} alignment="center">
|
||||
<Button style="ctaDarkYellow" onClick={saveAndExit}>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
@ -25,9 +25,9 @@ type HighlightsLayerProps = {
|
||||
articleAuthor: string
|
||||
isAppleAppEmbed: boolean
|
||||
highlightBarDisabled: boolean
|
||||
showNotesSidebar: boolean
|
||||
showHighlightsModal: boolean
|
||||
highlightsBaseURL: string
|
||||
setShowNotesSidebar: React.Dispatch<React.SetStateAction<boolean>>
|
||||
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
articleMutations: ArticleMutations
|
||||
}
|
||||
|
||||
@ -466,11 +466,11 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
if (props.showNotesSidebar) {
|
||||
if (props.showHighlightsModal) {
|
||||
return (
|
||||
<HighlightsModal
|
||||
highlights={highlights}
|
||||
onOpenChange={() => props.setShowNotesSidebar(false)}
|
||||
onOpenChange={() => props.setShowHighlightsModal(false)}
|
||||
deleteHighlightAction={(highlightId: string) => {
|
||||
removeHighlightCallback(highlightId)
|
||||
}}
|
||||
|
||||
@ -30,6 +30,7 @@ export function HighlightsModal(props: HighlightsModalProps): JSX.Element {
|
||||
<ModalContent
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
css={{ overflow: 'auto', p: '0' }}
|
||||
>
|
||||
|
||||
@ -0,0 +1,130 @@
|
||||
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 { AlignCenterHorizontalSimple, ArrowsInLineHorizontal, ArrowsOutLineHorizontal, Minus, Pen, Plus, Trash, X } from 'phosphor-react'
|
||||
import { AIcon } from '../../elements/images/AIcon'
|
||||
import { TickedRangeSlider } from '../../elements/TickedRangeSlider'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
|
||||
|
||||
type ReaderSettingsProps = {
|
||||
marginWidth: number
|
||||
lineHeight: number
|
||||
articleActionHandler: (action: string, arg?: number) => void
|
||||
}
|
||||
|
||||
const VerticalDivider = styled(SpanBox, {
|
||||
width: '1px',
|
||||
height: '100%',
|
||||
background: `${theme.colors.grayLine.toString()}`,
|
||||
})
|
||||
|
||||
export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element {
|
||||
const [lineHeight, setLineHeight] = useState(props.lineHeight)
|
||||
const [marginWidth, setMarginWidth] = useState(props.marginWidth)
|
||||
|
||||
useEffect(() => {
|
||||
setLineHeight(props.lineHeight)
|
||||
setMarginWidth(props.marginWidth)
|
||||
}, [props.lineHeight, props.marginWidth, setLineHeight, setMarginWidth])
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<HStack
|
||||
alignment='center'
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '70px',
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
}}
|
||||
>
|
||||
<Button style='plainIcon' onClick={() => props.articleActionHandler('decrementFontSize')}>
|
||||
<AIcon size={28} color={theme.colors.readerFont.toString()} />
|
||||
<Minus size={28} color={theme.colors.readerFont.toString()}/>
|
||||
</Button>
|
||||
<VerticalDivider />
|
||||
<Button style='plainIcon' onClick={() => props.articleActionHandler('incrementFontSize')}>
|
||||
<AIcon size={44} color={theme.colors.readerFont.toString()} />
|
||||
<Plus size={28} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
</HStack>
|
||||
<VStack
|
||||
css={{
|
||||
p: '0px',
|
||||
m: '0px',
|
||||
pb: '14px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledText color={theme.colors.readerFontTransparent.toString()} css={{ pl: '8px', m: '0px', pt: '14px' }}>Margin:</StyledText>
|
||||
<HStack distribution='between' css={{ gap: '16px', alignItems: 'center', alignSelf: 'center' }}>
|
||||
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }} onClick={() => {
|
||||
const newMarginWith = Math.max(marginWidth - 45, 200)
|
||||
setMarginWidth(newMarginWith)
|
||||
props.articleActionHandler('setMarginWidth', newMarginWith)
|
||||
}}>
|
||||
<ArrowsOutLineHorizontal size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
<TickedRangeSlider min={200} max={560} step={45} value={marginWidth} onChange={(value) => {
|
||||
setMarginWidth(value)
|
||||
props.articleActionHandler('setMarginWidth', value)
|
||||
}} />
|
||||
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }} onClick={() => {
|
||||
const newMarginWith = Math.min(marginWidth + 45, 560)
|
||||
setMarginWidth(newMarginWith)
|
||||
props.articleActionHandler('setMarginWidth', newMarginWith)
|
||||
}}>
|
||||
<ArrowsInLineHorizontal size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
|
||||
</HStack>
|
||||
</VStack>
|
||||
<VStack css={{
|
||||
p: '0px',
|
||||
m: '0px',
|
||||
pb: '12px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}>
|
||||
<StyledText color={theme.colors.readerFontTransparent.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', px: '4px' }} onClick={() => {
|
||||
const newLineHeight = Math.max(lineHeight - 25, 100)
|
||||
setLineHeight(newLineHeight)
|
||||
props.articleActionHandler('setLineHeight', newLineHeight)
|
||||
}}>
|
||||
<AlignCenterHorizontalSimple size={25} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
<TickedRangeSlider min={100} max={300} step={25} value={lineHeight} onChange={(value) => {
|
||||
setLineHeight(value)
|
||||
props.articleActionHandler('setLineHeight', value)
|
||||
}} />
|
||||
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }} onClick={() => {
|
||||
const newLineHeight = Math.min(lineHeight + 25, 300)
|
||||
setLineHeight(newLineHeight)
|
||||
props.articleActionHandler('setLineHeight', newLineHeight)
|
||||
}}>
|
||||
<AlignCenterHorizontalSimple size={25} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<Button style='plainIcon' css={{ justifyContent: 'center', textDecoration: 'underline', display: 'flex', gap: '4px', width: '100%', fontSize: '12px', p: '8px', pb: '0px', pt: '16px', height: '42px', alignItems: 'center' }}
|
||||
onClick={() => {
|
||||
setMarginWidth(290)
|
||||
setLineHeight(150)
|
||||
props.articleActionHandler('resetReaderSettings')
|
||||
showSuccessToast('Display settings reset', { position: 'bottom-right' })
|
||||
}}
|
||||
>
|
||||
Reset to default
|
||||
</Button>
|
||||
</VStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
345
packages/web/components/templates/article/SetLabelsControl.tsx
Normal file
345
packages/web/components/templates/article/SetLabelsControl.tsx
Normal file
@ -0,0 +1,345 @@
|
||||
import { useCallback, useRef, useState, useMemo, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Check, Circle, PencilSimple, Plus } from 'phosphor-react'
|
||||
import { isTouchScreenDevice } from '../../../lib/deviceType'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
type SetLabelsControlProps = {
|
||||
article: ArticleAttributes
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
filterText: string
|
||||
focused: boolean
|
||||
resetFocusedIndex: () => void
|
||||
setFilterText: (text: string) => void
|
||||
}
|
||||
|
||||
const FormInput = styled('input', {
|
||||
width: '100%',
|
||||
fontSize: '16px',
|
||||
fontFamily: 'inter',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1.8',
|
||||
color: '$grayTextContrast',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledLabel = styled('label', {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
})
|
||||
|
||||
function Header(props: HeaderProps): JSX.Element {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTouchScreenDevice() && props.focused && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [props.focused])
|
||||
|
||||
return (
|
||||
<VStack css={{ width: '100%', my: '0px', borderBottom: '1px solid $grayBorder'}}>
|
||||
<Box css={{
|
||||
width: '100%',
|
||||
my: '14px',
|
||||
px: '14px',
|
||||
}}>
|
||||
<FormInput
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
tabIndex={props.focused && !isTouchScreenDevice() ? 0 : -1}
|
||||
autoFocus={!isTouchScreenDevice()}
|
||||
value={props.filterText}
|
||||
placeholder="Filter for label"
|
||||
onChange={(event) => {
|
||||
props.setFilterText(event.target.value)
|
||||
}}
|
||||
onFocus={() => {
|
||||
props.resetFocusedIndex()
|
||||
}}
|
||||
css={{
|
||||
border: '1px solid $grayBorder',
|
||||
borderRadius: '8px',
|
||||
width: '100%',
|
||||
bg: 'transparent',
|
||||
fontSize: '16px',
|
||||
textIndent: '8px',
|
||||
marginBottom: '2px',
|
||||
color: '$grayTextContrast',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>)
|
||||
}
|
||||
|
||||
type LabelListItemProps = {
|
||||
label: Label
|
||||
focused: boolean
|
||||
selected: boolean
|
||||
toggleLabel: (label: Label) => void
|
||||
}
|
||||
|
||||
function LabelListItem(props: LabelListItemProps): JSX.Element {
|
||||
const ref = useRef<HTMLLabelElement>(null)
|
||||
const { label, focused, selected } = props
|
||||
|
||||
useEffect(() => {
|
||||
if (props.focused && ref.current) {
|
||||
ref.current.focus()
|
||||
}
|
||||
}, [props.focused])
|
||||
|
||||
return (
|
||||
<StyledLabel
|
||||
ref={ref}
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '42px',
|
||||
borderBottom: '1px solid $grayBorder',
|
||||
bg: props.focused ? '$grayBgActive' : 'unset',
|
||||
}}
|
||||
tabIndex={props.focused ? 0 : -1}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
props.toggleLabel(label)
|
||||
ref.current?.blur()
|
||||
}}
|
||||
>
|
||||
<input autoFocus={focused} hidden={true} type="checkbox" checked={selected} readOnly />
|
||||
<Box css={{ pl: '10px', width: '32px', display: 'flex', alignItems: 'center' }}>
|
||||
{selected && <Check size={15} color={theme.colors.grayText.toString()} weight='bold' />}
|
||||
</Box>
|
||||
<Box css={{ width: '30px', height: '100%', display: 'flex', alignItems: 'center' }}>
|
||||
<Circle width={22} height={22} color={label.color} weight='fill' />
|
||||
</Box>
|
||||
<Box css={{ overflow: 'clip', height: '100%', display: 'flex', alignItems: 'center' }}>
|
||||
<StyledText style="caption">{label.name}</StyledText>
|
||||
</Box>
|
||||
<Box css={{ pl: '10px', width: '40px', marginLeft: 'auto', display: 'flex', alignItems: 'center' }}>
|
||||
{selected && <CrossIcon
|
||||
size={14}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>}
|
||||
</Box>
|
||||
</StyledLabel>
|
||||
)
|
||||
}
|
||||
|
||||
type FooterProps = {
|
||||
focused: boolean
|
||||
}
|
||||
|
||||
function Footer(props: FooterProps): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.focused && ref.current) {
|
||||
ref.current.focus()
|
||||
}
|
||||
}, [props.focused])
|
||||
|
||||
return (
|
||||
<HStack
|
||||
ref={ref}
|
||||
distribution="start" alignment="center"
|
||||
css={{
|
||||
width: '100%', height: '42px',
|
||||
bg: props.focused ? '$grayBgActive' : 'unset',
|
||||
color: theme.colors.grayText.toString(),
|
||||
'a:link': {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
'a:visited': {
|
||||
color: theme.colors.grayText.toString(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SpanBox css={{ display: 'flex', fontSize: '12px', padding: '33px', gap: '8px' }}>
|
||||
<PencilSimple size={18} color={theme.colors.grayText.toString()} />
|
||||
<Link href="/settings/labels">Edit labels</Link>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [filterText, setFilterText] = useState('')
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
const [selectedLabels, setSelectedLabels] = useState<Label[]>(props.article.labels || [])
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedIndex(undefined)
|
||||
}, [filterText])
|
||||
|
||||
const isSelected = useCallback((label: Label): boolean => {
|
||||
return selectedLabels.some((other) => {
|
||||
return other.id === label.id
|
||||
})
|
||||
}, [selectedLabels])
|
||||
|
||||
const toggleLabel = useCallback(async (label: Label) => {
|
||||
let newSelectedLabels = [...selectedLabels]
|
||||
if (isSelected(label)) {
|
||||
newSelectedLabels = selectedLabels.filter((other) => {
|
||||
return other.id !== label.id
|
||||
})
|
||||
} else {
|
||||
newSelectedLabels = [...selectedLabels, label]
|
||||
}
|
||||
setSelectedLabels(newSelectedLabels)
|
||||
|
||||
const result = await setLabelsMutation(
|
||||
props.article.linkId,
|
||||
newSelectedLabels.map((label) => label.id)
|
||||
)
|
||||
|
||||
props.article.labels = result
|
||||
props.articleActionHandler('refreshLabels', result)
|
||||
|
||||
revalidate()
|
||||
}, [isSelected, selectedLabels, setSelectedLabels])
|
||||
|
||||
const filteredLabels = useMemo(() => {
|
||||
if (!labels) {
|
||||
return []
|
||||
}
|
||||
return labels.filter((label) => {
|
||||
return label.name.toLowerCase().includes(filterText.toLowerCase())
|
||||
})
|
||||
}, [labels, filterText])
|
||||
|
||||
// Move focus through the labels list on tab or arrow up/down keys
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(undefined)
|
||||
const handleKeyDown = useCallback(async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const maxIndex = filteredLabels.length + 1
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
let newIndex = focusedIndex
|
||||
if (focusedIndex) {
|
||||
newIndex = Math.max(0, focusedIndex - 1)
|
||||
} else {
|
||||
newIndex = undefined
|
||||
}
|
||||
// If the `Create New label` button isn't visible we skip it
|
||||
// when navigating with the arrow keys
|
||||
if (focusedIndex === maxIndex && !filterText) {
|
||||
newIndex = maxIndex - 2
|
||||
}
|
||||
setFocusedIndex(newIndex)
|
||||
}
|
||||
if (event.key === 'ArrowDown' || event.key === 'Tab') {
|
||||
event.preventDefault()
|
||||
let newIndex = focusedIndex
|
||||
if (focusedIndex === undefined) {
|
||||
newIndex = 0
|
||||
} else {
|
||||
newIndex = Math.min(maxIndex, focusedIndex + 1)
|
||||
}
|
||||
// If the `Create New label` button isn't visible we skip it
|
||||
// when navigating with the arrow keys
|
||||
if (focusedIndex === maxIndex - 2 && !filterText) {
|
||||
newIndex = maxIndex
|
||||
}
|
||||
setFocusedIndex(newIndex)
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
if (focusedIndex === maxIndex) {
|
||||
router.push('/settings/labels')
|
||||
return
|
||||
}
|
||||
if (focusedIndex === maxIndex - 1) {
|
||||
await createLabelFromFilterText()
|
||||
return
|
||||
}
|
||||
if (focusedIndex !== undefined) {
|
||||
const label = filteredLabels[focusedIndex]
|
||||
if (label) {
|
||||
toggleLabel(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [filterText, filteredLabels, focusedIndex, isSelected, selectedLabels, setSelectedLabels])
|
||||
|
||||
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, selectedLabels, setSelectedLabels, toggleLabel])
|
||||
|
||||
return (
|
||||
<VStack
|
||||
distribution="start"
|
||||
onKeyDown={handleKeyDown}
|
||||
css={{
|
||||
p: '0',
|
||||
width: '100%',
|
||||
}}>
|
||||
<Header
|
||||
focused={focusedIndex === undefined}
|
||||
resetFocusedIndex={() => setFocusedIndex(undefined)}
|
||||
setFilterText={setFilterText} filterText={filterText}
|
||||
/>
|
||||
<VStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{ flexGrow: '1', overflow: 'scroll', width: '100%', height: '100%', maxHeight: '294px'
|
||||
}}>
|
||||
{filteredLabels &&
|
||||
filteredLabels.map((label, idx) => (
|
||||
<LabelListItem
|
||||
key={label.id}
|
||||
label={label}
|
||||
focused={idx === focusedIndex}
|
||||
selected={isSelected(label)}
|
||||
toggleLabel={toggleLabel}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
{filterText && (
|
||||
<Button style='modalOption' css={{
|
||||
pl: '26px',
|
||||
color: theme.colors.grayText.toString(),
|
||||
height: '42px',
|
||||
borderBottom: '1px solid $grayBorder',
|
||||
bg: focusedIndex === filteredLabels.length ? '$grayBgActive' : 'unset',
|
||||
}}
|
||||
onClick={createLabelFromFilterText}
|
||||
>
|
||||
<HStack alignment='center' distribution='start' css={{ gap: '8px' }}>
|
||||
<Plus size={18} color={theme.colors.grayText.toString()} />
|
||||
<SpanBox css={{ fontSize: '12px' }}>{`Create new label "${filterText}"`}</SpanBox>
|
||||
</HStack>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Footer focused={focusedIndex === (filteredLabels.length + 1)} />
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
56
packages/web/components/templates/article/SetLabelsModal.tsx
Normal file
56
packages/web/components/templates/article/SetLabelsModal.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import {
|
||||
ModalRoot,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { SetLabelsControl } from './SetLabelsControl'
|
||||
|
||||
type SetLabelsModalProps = {
|
||||
article: ArticleAttributes
|
||||
onOpenChange: (open: boolean) => void
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
}
|
||||
|
||||
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
css={{ border: '1px solid $grayBorder' }}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<VStack css={{ width: '100%' }}>
|
||||
<HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<StyledText style="modalHeadline" css={{ pl: '16px' }}>Labels</StyledText>
|
||||
<Button
|
||||
css={{ pt: '16px', pr: '16px' }}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<CrossIcon
|
||||
size={14}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
<SetLabelsControl {...props} />
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
@ -102,8 +102,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
||||
borderStyles: {},
|
||||
shadows: {
|
||||
panelShadow: '0px 4px 18px rgba(120, 123, 134, 0.12)',
|
||||
cardBoxShadow:
|
||||
'0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)',
|
||||
cardBoxShadow: '0px 0px 4px 0px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
zIndices: {},
|
||||
transitions: {},
|
||||
@ -156,6 +155,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
||||
xsmDown: '(max-width: 375px)',
|
||||
smDown: '(max-width: 575px)',
|
||||
mdDown: '(max-width: 768px)',
|
||||
lgDown: '(max-width: 992px)',
|
||||
sm: '(min-width: 576px)',
|
||||
md: '(min-width: 768px)',
|
||||
lg: '(min-width: 992px)',
|
||||
@ -199,7 +199,7 @@ const darkThemeSpec = {
|
||||
},
|
||||
shadows: {
|
||||
cardBoxShadow:
|
||||
'0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)',
|
||||
'0px 0px 9px -2px rgba(255, 255, 255, 0.09), 0px 7px 12px rgba(255, 255, 255, 0.07)',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -207,7 +207,9 @@ type ArticleKeyboardAction =
|
||||
| 'openOriginalArticle'
|
||||
| 'incrementFontSize'
|
||||
| 'decrementFontSize'
|
||||
| 'editLabels'
|
||||
| 'incrementMarginWidth'
|
||||
| 'decrementMarginWidth'
|
||||
| 'setLabels'
|
||||
|
||||
export function articleKeyboardCommands(
|
||||
router: NextRouter | undefined,
|
||||
@ -238,11 +240,23 @@ export function articleKeyboardCommands(
|
||||
shortcutKeyDescription: '-',
|
||||
callback: () => actionHandler('decrementFontSize'),
|
||||
},
|
||||
{
|
||||
shortcutKeys: [']'],
|
||||
actionDescription: 'Increase margin width',
|
||||
shortcutKeyDescription: ']',
|
||||
callback: () => actionHandler('incrementMarginWidth'),
|
||||
},
|
||||
{
|
||||
shortcutKeys: ['['],
|
||||
actionDescription: 'Decrease margin width',
|
||||
shortcutKeyDescription: '[',
|
||||
callback: () => actionHandler('decrementMarginWidth'),
|
||||
},
|
||||
{
|
||||
shortcutKeys: ['l'],
|
||||
actionDescription: 'Edit labels',
|
||||
shortcutKeyDescription: 'l',
|
||||
callback: () => actionHandler('editLabels'),
|
||||
callback: () => actionHandler('setLabels'),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export type LabelColor =
|
||||
| '#FF5D99'
|
||||
| '#7CFF7B'
|
||||
| '#FFD234'
|
||||
| '#7BE4FF'
|
||||
| '#CE88EF'
|
||||
| '#EF8C43'
|
||||
| 'custom color';
|
||||
|
||||
export const labelFragment = gql`
|
||||
fragment LabelFields on Label {
|
||||
id
|
||||
@ -13,7 +22,7 @@ export const labelFragment = gql`
|
||||
export type Label = {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
color: LabelColor
|
||||
description?: string
|
||||
createdAt: string
|
||||
}
|
||||
createdAt: Date
|
||||
}
|
||||
@ -1,6 +1,16 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type CreateLabelResult = {
|
||||
createLabel: CreateLabel
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type CreateLabel = {
|
||||
label: Label
|
||||
}
|
||||
|
||||
export async function createLabelMutation(
|
||||
name: string,
|
||||
color: string,
|
||||
@ -32,9 +42,9 @@ export async function createLabelMutation(
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation)
|
||||
const data = await gqlFetcher(mutation) as CreateLabelResult
|
||||
console.log('created label', data)
|
||||
return data
|
||||
return data.errorCodes ? undefined : data.createLabel.label
|
||||
} catch (error) {
|
||||
console.log('createLabelMutation error', error)
|
||||
return undefined
|
||||
|
||||
@ -1,16 +1,26 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type SetLabelsResult = {
|
||||
setLabels: SetLabels
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type SetLabels = {
|
||||
labels: Label[]
|
||||
}
|
||||
|
||||
export async function setLabelsMutation(
|
||||
pageId: string,
|
||||
labelIds: string[]
|
||||
): Promise<any | undefined> {
|
||||
): Promise<Label[] | undefined> {
|
||||
const mutation = gql`
|
||||
mutation SetLabels($input: SetLabelsInput!) {
|
||||
setLabels(input: $input) {
|
||||
... on SetLabelsSuccess {
|
||||
labels {
|
||||
id
|
||||
...LabelFields
|
||||
}
|
||||
}
|
||||
... on SetLabelsError {
|
||||
@ -18,12 +28,12 @@ export async function setLabelsMutation(
|
||||
}
|
||||
}
|
||||
}
|
||||
${labelFragment}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, { input: { pageId, labelIds } })
|
||||
console.log(data)
|
||||
return data
|
||||
const data = await gqlFetcher(mutation, { input: { pageId, labelIds } }) as SetLabelsResult
|
||||
return data.errorCodes ? undefined : data.setLabels.labels
|
||||
} catch (error) {
|
||||
console.log('SetLabelsOutput error', error)
|
||||
return undefined
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import useSWRImmutable from 'swr'
|
||||
import useSWRImmutable, { Cache } from 'swr'
|
||||
import { makeGqlFetcher, RequestContext, ssrFetcher } from '../networkHelpers'
|
||||
import { articleFragment, ContentReader } from '../fragments/articleFragment'
|
||||
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
|
||||
import { ScopedMutator } from 'swr/dist/types'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { LibraryItems } from './useGetLibraryItemsQuery'
|
||||
|
||||
type ArticleQueryInput = {
|
||||
username?: string
|
||||
@ -77,16 +78,7 @@ const query = gql`
|
||||
${highlightFragment}
|
||||
${labelFragment}
|
||||
`
|
||||
export const cacheArticle = (
|
||||
mutate: ScopedMutator,
|
||||
username: string,
|
||||
article: ArticleAttributes,
|
||||
includeFriendsHighlights = false
|
||||
) => {
|
||||
mutate([query, username, article.slug, includeFriendsHighlights], {
|
||||
article: { article: { ...article, cached: true } },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function useGetArticleQuery({
|
||||
username,
|
||||
@ -99,7 +91,7 @@ export function useGetArticleQuery({
|
||||
includeFriendsHighlights,
|
||||
}
|
||||
|
||||
const { data, error } = useSWRImmutable(
|
||||
const { data, error, mutate } = useSWRImmutable(
|
||||
slug ? [query, username, slug, includeFriendsHighlights] : null,
|
||||
makeGqlFetcher(variables)
|
||||
)
|
||||
@ -133,3 +125,46 @@ export async function articleQuery(
|
||||
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
export const cacheArticle = (
|
||||
mutate: ScopedMutator,
|
||||
username: string,
|
||||
article: ArticleAttributes,
|
||||
includeFriendsHighlights = false
|
||||
) => {
|
||||
mutate([query, username, article.slug, includeFriendsHighlights], {
|
||||
article: { article: { ...article, cached: true } },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const removeItemFromCache = (
|
||||
cache: Cache<unknown>,
|
||||
mutate: ScopedMutator,
|
||||
itemId: string,
|
||||
) => {
|
||||
try {
|
||||
const mappedCache = cache as Map<string, unknown>
|
||||
mappedCache.forEach((value: any, key) => {
|
||||
if (typeof value == 'object' && 'articles' in value) {
|
||||
const articles = value.articles as LibraryItems
|
||||
const idx = articles.edges.findIndex((edge) => edge.node.id == itemId)
|
||||
if (idx > -1) {
|
||||
value.articles.edges.splice(idx, 1)
|
||||
mutate(key, value, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
mappedCache.forEach((value: any, key) => {
|
||||
if (Array.isArray(value)) {
|
||||
const idx = value.findIndex((item) => 'articles' in item)
|
||||
if (idx > -1) {
|
||||
mutate(key, value, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('error removing item from cache', error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { LabelColor } from '../../../utils/settings-page/labels/types';
|
||||
import { labelFragment } from '../fragments/labelFragment'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
type LabelsQueryResponse = {
|
||||
@ -18,14 +17,6 @@ type LabelsData = {
|
||||
labels?: unknown
|
||||
}
|
||||
|
||||
export type Label = {
|
||||
id: string
|
||||
name: string
|
||||
color: LabelColor
|
||||
description?: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export function useGetLabelsQuery(): LabelsQueryResponse {
|
||||
const query = gql`
|
||||
query GetLabels {
|
||||
|
||||
@ -6,7 +6,7 @@ import { articleFragment } from '../fragments/articleFragment'
|
||||
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
|
||||
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
|
||||
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
|
||||
import { Label } from './../fragments/labelFragment'
|
||||
import { Label, labelFragment } from './../fragments/labelFragment'
|
||||
import { showErrorToast, showSuccessToast } from '../../toastHelpers'
|
||||
|
||||
export type LibraryItemsQueryInput = {
|
||||
@ -40,7 +40,7 @@ export type LibraryItemsData = {
|
||||
articles: LibraryItems
|
||||
}
|
||||
|
||||
type LibraryItems = {
|
||||
export type LibraryItems = {
|
||||
edges: LibraryItem[]
|
||||
pageInfo: PageInfo
|
||||
errorCodes?: string[]
|
||||
@ -67,6 +67,30 @@ export type PageInfo = {
|
||||
totalCount: number
|
||||
}
|
||||
|
||||
const libraryItemFragment = gql`
|
||||
fragment ArticleFields on Article {
|
||||
id
|
||||
title
|
||||
url
|
||||
author
|
||||
image
|
||||
savedAt
|
||||
createdAt
|
||||
publishedAt
|
||||
contentReader
|
||||
originalArticleUrl
|
||||
readingProgressPercent
|
||||
readingProgressAnchorIndex
|
||||
slug
|
||||
isArchived
|
||||
description
|
||||
linkId
|
||||
labels {
|
||||
...LabelFields
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function useGetLibraryItemsQuery({
|
||||
limit,
|
||||
sortDescending,
|
||||
@ -109,7 +133,8 @@ export function useGetLibraryItemsQuery({
|
||||
}
|
||||
}
|
||||
}
|
||||
${articleFragment}
|
||||
${libraryItemFragment}
|
||||
${labelFragment}
|
||||
`
|
||||
|
||||
const variables = {
|
||||
|
||||
@ -51,7 +51,7 @@ export function useGetNewsletterEmailsQuery(): NewsletterEmailsQueryResponse {
|
||||
isValidating,
|
||||
emailAddresses,
|
||||
revalidate: () => {
|
||||
mutate()
|
||||
mutate(undefined, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ export type UserPreferences = {
|
||||
fontSize: number
|
||||
fontFamily: string
|
||||
margin: number
|
||||
lineHeight?: number
|
||||
libraryLayoutType: string
|
||||
librarySortOrder?: SortOrder
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { PrimaryLayout } from '../../../components/templates/PrimaryLayout'
|
||||
import { LoadingView } from '../../../components/patterns/LoadingView'
|
||||
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { removeItemFromCache, useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { useRouter } from 'next/router'
|
||||
import { VStack } from './../../../components/elements/LayoutPrimitives'
|
||||
import { ArticleContainer } from './../../../components/templates/article/ArticleContainer'
|
||||
import { PdfArticleContainerProps } from './../../../components/templates/article/PdfArticleContainer'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { articleKeyboardCommands, navigationCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import dynamic from 'next/dynamic'
|
||||
@ -20,9 +20,16 @@ import { articleReadingProgressMutation } from '../../../lib/networking/mutation
|
||||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation'
|
||||
import Script from 'next/script'
|
||||
import { EditLabelsModal } from '../../../components/templates/article/EditLabelsModal'
|
||||
import { theme } from '../../../components/tokens/stitches.config'
|
||||
import { ArticleActionsMenu } from '../../../components/templates/article/ArticleActionsMenu'
|
||||
import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { isVipUser } from '../../../lib/featureFlag'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { SetLabelsModal } from '../../../components/templates/article/SetLabelsModal'
|
||||
import { DisplaySettingsModal } from '../../../components/templates/article/DisplaySettingsModal'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
|
||||
|
||||
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
|
||||
() => import('./../../../components/templates/article/PdfArticleContainer'),
|
||||
@ -31,49 +38,121 @@ const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { cache, mutate } = useSWRConfig()
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null)
|
||||
const { slug } = router.query
|
||||
const [showLabelsModal, setShowLabelsModal] = useState(false)
|
||||
const [showHighlightsModal, setShowHighlightsModal] = useState(false)
|
||||
|
||||
// Populate data cache
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const { preferencesData } = useGetUserPreferences()
|
||||
const [fontSize, setFontSize] = useState(preferencesData?.fontSize ?? 20)
|
||||
const [lineHeight, setLineHeight] = usePersistedState({ key: 'lineHeight', initialValue: 150 })
|
||||
const [marginWidth, setMarginWidth] = usePersistedState({ key: 'marginWidth', initialValue: 200 })
|
||||
const [showSetLabelsModal, setShowSetLabelsModal] = useState(false)
|
||||
const [showEditDisplaySettingsModal, setShowEditDisplaySettingsModal] = useState(false)
|
||||
|
||||
const { articleData } = useGetArticleQuery({
|
||||
username: router.query.username as string,
|
||||
slug: router.query.slug as string,
|
||||
includeFriendsHighlights: false,
|
||||
})
|
||||
const { preferencesData } = useGetUserPreferences()
|
||||
const article = articleData?.article.article
|
||||
const [fontSize, setFontSize] = useState(preferencesData?.fontSize ?? 20)
|
||||
|
||||
const [labels, setLabels] = useState<Label[]>([])
|
||||
useEffect(() => {
|
||||
if (article?.labels) {
|
||||
setLabels(article.labels)
|
||||
}
|
||||
}, [article])
|
||||
|
||||
useKeyboardShortcuts(navigationCommands(router))
|
||||
|
||||
const updateFontSize = async (newFontSize: number) => {
|
||||
setFontSize(newFontSize)
|
||||
await userPersonalizationMutation({ fontSize: newFontSize })
|
||||
}
|
||||
const actionHandler = useCallback(async(action: string, arg?: unknown) => {
|
||||
const updateFontSize = async(newFontSize: number) => {
|
||||
setFontSize(newFontSize)
|
||||
await userPersonalizationMutation({ fontSize: newFontSize })
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'archive':
|
||||
if (article) {
|
||||
removeItemFromCache(cache, mutate, article.id)
|
||||
|
||||
await setLinkArchivedMutation({
|
||||
linkId: article.id,
|
||||
archived: true,
|
||||
}).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToast('Link archived', { position: 'bottom-right' })
|
||||
} else {
|
||||
// todo: revalidate or put back in cache?
|
||||
showErrorToast('Error archiving link', { position: 'bottom-right' })
|
||||
}
|
||||
})
|
||||
|
||||
router.push(`/home`)
|
||||
}
|
||||
break
|
||||
case 'openOriginalArticle':
|
||||
const url = article?.url
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
break
|
||||
case 'refreshLabels':
|
||||
setLabels(arg as Label[])
|
||||
break
|
||||
case 'showHighlights':
|
||||
setShowHighlightsModal(true)
|
||||
break
|
||||
case 'incrementFontSize':
|
||||
await updateFontSize(Math.min(fontSize + 2, 28))
|
||||
break
|
||||
case 'decrementFontSize':
|
||||
await updateFontSize(Math.max(fontSize - 2, 10))
|
||||
break
|
||||
case 'setMarginWidth': {
|
||||
const value = Number(arg)
|
||||
if (value >= 200 && value <= 560) {
|
||||
setMarginWidth(value)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'incrementMarginWidth':
|
||||
setMarginWidth(Math.min(marginWidth + 45, 560))
|
||||
break
|
||||
case 'decrementMarginWidth':
|
||||
setMarginWidth(Math.max(marginWidth - 45, 200))
|
||||
break
|
||||
case 'setLineHeight': {
|
||||
const value = Number(arg)
|
||||
if (value >= 100 && value <= 300) {
|
||||
setLineHeight(arg as number)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'editDisplaySettings': {
|
||||
setShowEditDisplaySettingsModal(true)
|
||||
break
|
||||
}
|
||||
case 'setLabels': {
|
||||
setShowSetLabelsModal(true)
|
||||
break
|
||||
}
|
||||
case 'resetReaderSettings': {
|
||||
updateFontSize(20)
|
||||
setMarginWidth(290)
|
||||
setLineHeight(150)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [article, cache, mutate, router,
|
||||
fontSize, setFontSize, lineHeight,
|
||||
setLineHeight, marginWidth, setMarginWidth])
|
||||
|
||||
useKeyboardShortcuts(
|
||||
articleKeyboardCommands(router, async (action) => {
|
||||
switch (action) {
|
||||
case 'openOriginalArticle':
|
||||
const url = article?.url
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
break
|
||||
case 'incrementFontSize':
|
||||
await updateFontSize(Math.min(fontSize + 2, 28))
|
||||
break
|
||||
case 'decrementFontSize':
|
||||
await updateFontSize(Math.max(fontSize - 2, 10))
|
||||
break
|
||||
case 'editLabels':
|
||||
if (viewerData?.me && isVipUser(viewerData?.me)) {
|
||||
setShowLabelsModal(true)
|
||||
}
|
||||
break
|
||||
}
|
||||
actionHandler(action)
|
||||
})
|
||||
)
|
||||
|
||||
@ -82,7 +161,15 @@ export default function Home(): JSX.Element {
|
||||
<PrimaryLayout
|
||||
pageTestId="home-page-tag"
|
||||
scrollElementRef={scrollRef}
|
||||
displayFontStepper={true}
|
||||
headerToolbarControl={
|
||||
<ArticleActionsMenu
|
||||
article={article}
|
||||
layout='horizontal'
|
||||
lineHeight={lineHeight}
|
||||
marginWidth={marginWidth}
|
||||
articleActionHandler={actionHandler}
|
||||
/>
|
||||
}
|
||||
pageMetaDataProps={{
|
||||
title: article.title,
|
||||
path: router.pathname,
|
||||
@ -97,6 +184,28 @@ export default function Home(): JSX.Element {
|
||||
/>
|
||||
<Toaster />
|
||||
|
||||
<VStack distribution="between" alignment="center" css={{
|
||||
position: 'fixed',
|
||||
flexDirection: 'row-reverse',
|
||||
top: '-120px',
|
||||
left: 8,
|
||||
height: '100%',
|
||||
width: '48px',
|
||||
'@lgDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{article.contentReader !== 'PDF' ? (
|
||||
<ArticleActionsMenu
|
||||
article={article}
|
||||
layout='vertical'
|
||||
lineHeight={lineHeight}
|
||||
marginWidth={marginWidth}
|
||||
articleActionHandler={actionHandler}
|
||||
/>
|
||||
) : null}
|
||||
</VStack>
|
||||
{article.contentReader == 'PDF' ? (
|
||||
<PdfArticleContainerNoSSR
|
||||
article={article}
|
||||
@ -104,10 +213,15 @@ export default function Home(): JSX.Element {
|
||||
/>
|
||||
) : (
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
ref={scrollRef}
|
||||
className="disable-webkit-callout"
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
ref={scrollRef}
|
||||
className="disable-webkit-callout"
|
||||
css={{
|
||||
'@smDown': {
|
||||
background: theme.colors.grayBg.toString(),
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArticleContainer
|
||||
article={article}
|
||||
@ -116,6 +230,11 @@ export default function Home(): JSX.Element {
|
||||
highlightBarDisabled={false}
|
||||
highlightsBaseURL={`${webBaseURL}/${viewerData.me?.profile?.username}/${slug}/highlights`}
|
||||
fontSize={fontSize}
|
||||
margin={marginWidth}
|
||||
lineHeight={lineHeight}
|
||||
labels={labels}
|
||||
showHighlightsModal={showHighlightsModal}
|
||||
setShowHighlightsModal={setShowHighlightsModal}
|
||||
articleMutations={{
|
||||
createHighlightMutation,
|
||||
deleteHighlightMutation,
|
||||
@ -124,20 +243,25 @@ export default function Home(): JSX.Element {
|
||||
articleReadingProgressMutation,
|
||||
}}
|
||||
/>
|
||||
{/* {showLabelsModal && (
|
||||
<EditLabelsModal
|
||||
labels={article.labels || []}
|
||||
article={article}
|
||||
onOpenChange={() => {
|
||||
setShowLabelsModal(false)
|
||||
}}
|
||||
setLabels={(labels: Label[]) => {
|
||||
// setLabels(labels)
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{showSetLabelsModal && (
|
||||
<SetLabelsModal
|
||||
article={article}
|
||||
articleActionHandler={actionHandler}
|
||||
onOpenChange={() => setShowSetLabelsModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showEditDisplaySettingsModal && (
|
||||
<DisplaySettingsModal
|
||||
lineHeight={lineHeight}
|
||||
marginWidth={marginWidth}
|
||||
articleActionHandler={actionHandler}
|
||||
onOpenChange={() => setShowEditDisplaySettingsModal(false)}
|
||||
/>
|
||||
)}
|
||||
</PrimaryLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -63,6 +63,7 @@ function AppArticleEmbedContent(
|
||||
props: AppArticleEmbedContentProps
|
||||
): JSX.Element {
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null)
|
||||
const [showHighlightsModal, setShowHighlightsModal] = useState(false)
|
||||
|
||||
const { articleData } = useGetArticleQuery({
|
||||
username: props.username,
|
||||
@ -99,6 +100,9 @@ function AppArticleEmbedContent(
|
||||
fontSize={props.fontSize}
|
||||
margin={props.margin}
|
||||
fontFamily={props.fontFamily}
|
||||
labels={[]}
|
||||
showHighlightsModal={showHighlightsModal}
|
||||
setShowHighlightsModal={setShowHighlightsModal}
|
||||
articleMutations={{
|
||||
createHighlightMutation,
|
||||
deleteHighlightMutation,
|
||||
|
||||
89
packages/web/pages/help/labels.tsx
Normal file
89
packages/web/pages/help/labels.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { Box, HStack } from '../../components/elements/LayoutPrimitives'
|
||||
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Labels(): JSX.Element {
|
||||
return (
|
||||
<PrimaryLayout
|
||||
pageMetaDataProps={{
|
||||
title: 'Labels',
|
||||
path: '/help/labels',
|
||||
}}
|
||||
pageTestId="help-labels-tag"
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
m: '42px',
|
||||
maxWidth: '640px',
|
||||
color: '$grayText',
|
||||
img: {
|
||||
maxWidth: '85%',
|
||||
},
|
||||
'@smDown': {
|
||||
m: '16px',
|
||||
maxWidth: '85%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<h1>Organize your Omnivore library with labels</h1>
|
||||
<hr />
|
||||
<h2>Introduction</h2>
|
||||
<p>
|
||||
Labels allow you to group and search for content in Omnivore. A saved page
|
||||
can have multiple labels and search results can be filtered by label.
|
||||
</p>
|
||||
<h2>Adding labels to a page on iOS</h2>
|
||||
<p>
|
||||
On iOS you add and remove labels from a page using the Assign Labels modal.
|
||||
</p>
|
||||
<p>
|
||||
You can open the Assign Labels modal from the home view or the reader view.
|
||||
In the home view long press on an item and choose Edit Labels from the dropdowm
|
||||
menu. In the reader view use the top right menu button.
|
||||
</p>
|
||||
<h2>Adding labels to a page on the web</h2>
|
||||
<p>
|
||||
On the web you add and remove labels from a page using the Assign Labels dropdown
|
||||
or modal depending on your screen size. For larger monitors you will see the Labels
|
||||
button on the left hand side of the reader view. For smaller monitors you will see
|
||||
the labels dropdown at the top of your screen.
|
||||
</p>
|
||||
<p>
|
||||
You can also use keyboard commands to open the assign labels modal. On the reader
|
||||
view tap the <code>l</code> key. Once open you can use the up/down arrow keys, or the
|
||||
tab key to navigate the available labels, and the Enter key to toggle a label.
|
||||
</p>
|
||||
<h2>Searching by label on iOS</h2>
|
||||
<p>
|
||||
On iOS you can use the Labels search chip to search for labels. This will open a modal
|
||||
allowing you to assign multiple labels to your search. This will become an <code>OR </code>
|
||||
search, meaning if you add multiple labels to your search, pages that have any of the
|
||||
labels will be returned.
|
||||
</p>
|
||||
<h2>Searching by label with Advanced Search</h2>
|
||||
<p>
|
||||
Omnivore's advanced search syntax supports searching for multiple labels using
|
||||
<code>AND</code> and <code>OR</code> clauses. You can also negate a label search
|
||||
to find all pages that do not have a certain label.
|
||||
</p>
|
||||
<p>Some examples:</p>
|
||||
<ul>
|
||||
<li><code>-label:Newsletter</code> finds all pages that do not have the label <code>Newsletter</code></li>
|
||||
<li><code>label:Cooking,Fitness</code> finds all your pages with either the <code>Cooking</code> or <code>Fitness</code> labels</li>
|
||||
<li><code>label:Newsletter label:Surfing</code> finds all pages with both the <code>Newsletter</code> and <code>Surfing</code> labels</li>
|
||||
<li><code>label:Coding -label:Newsletter</code> finds all pages with the <code>Coding</code> label that do not have the <code>Newsletter</code> label</li>
|
||||
</ul>
|
||||
|
||||
<h2>Editing your list of labels</h2>
|
||||
<p>
|
||||
The <Link href="/settings/labels"><a>labels</a></Link> page allows you to
|
||||
edit all of your labels. From here you can create new labels, delete existing
|
||||
labels, or modify the color and description of a label.
|
||||
</p>
|
||||
</Box>
|
||||
<Box css={{ height: '120px' }} />
|
||||
</PrimaryLayout>
|
||||
)
|
||||
}
|
||||
@ -15,8 +15,7 @@ import { updateLabelMutation } from '../../lib/networking/mutations/updateLabelM
|
||||
import { deleteLabelMutation } from '../../lib/networking/mutations/deleteLabelMutation'
|
||||
import { applyStoredTheme, isDarkTheme } from '../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { Label } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
|
||||
import { Label, LabelColor } from '../../lib/networking/fragments/labelFragment'
|
||||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import {
|
||||
ArrowClockwise,
|
||||
@ -26,7 +25,6 @@ import {
|
||||
Plus,
|
||||
} from 'phosphor-react'
|
||||
import {
|
||||
LabelColor,
|
||||
GenericTableCardProps,
|
||||
LabelColorHex,
|
||||
} from '../../utils/settings-page/labels/types'
|
||||
@ -185,18 +183,14 @@ export default function LabelsPage(): JSX.Element {
|
||||
|
||||
async function createLabel(): Promise<void> {
|
||||
const res = await createLabelMutation(
|
||||
nameInputText,
|
||||
nameInputText.trim(),
|
||||
labelColorHex.value,
|
||||
descriptionInputText
|
||||
)
|
||||
if (res) {
|
||||
if (res.createLabel.errorCodes && res.createLabel.errorCodes.length > 0) {
|
||||
showErrorToast(res.createLabel.errorCodes[0])
|
||||
} else {
|
||||
showSuccessToast('Label created', { position: 'bottom-right' })
|
||||
resetLabelState()
|
||||
revalidate()
|
||||
}
|
||||
showSuccessToast('Label created', { position: 'bottom-right' })
|
||||
resetLabelState()
|
||||
revalidate()
|
||||
} else {
|
||||
showErrorToast('Failed to create label')
|
||||
}
|
||||
|
||||
@ -99,3 +99,44 @@ div#appleid-signin {
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--colors-grayBorderHover);
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px !important;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--colors-utilityTextContrast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px !important;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--colors-utilityTextContrast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--colors-grayTextContrast);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
@ -44,3 +44,9 @@ export const labelColorObjects: LabelColorObjects = {
|
||||
background: '#D8D7D50D',
|
||||
},
|
||||
}
|
||||
|
||||
export const randomLabelColorHex = () => {
|
||||
const colorHexes = Object.keys(labelColorObjects).slice(0, -1)
|
||||
const randomColorHex = colorHexes[Math.floor(Math.random() * colorHexes.length)]
|
||||
return randomColorHex
|
||||
}
|
||||
@ -1,14 +1,4 @@
|
||||
import React from "react";
|
||||
import { Label } from "../../../lib/networking/queries/useGetLabelsQuery";
|
||||
|
||||
export type LabelColor =
|
||||
| '#FF5D99'
|
||||
| '#7CFF7B'
|
||||
| '#FFD234'
|
||||
| '#7BE4FF'
|
||||
| '#CE88EF'
|
||||
| '#EF8C43'
|
||||
| 'custom color';
|
||||
import { Label, LabelColor } from "../../../lib/networking/fragments/labelFragment";
|
||||
|
||||
export type LabelOptionProps = {
|
||||
color: string;
|
||||
|
||||
Reference in New Issue
Block a user