diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index b98ff016c..15fd1f92c 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -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 } } } diff --git a/packages/appreader/src/index.jsx b/packages/appreader/src/index.jsx index 18aba5c8b..3f0c815cd 100644 --- a/packages/appreader/src/index.jsx +++ b/packages/appreader/src/index.jsx @@ -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), diff --git a/packages/web/components/elements/Avatar.tsx b/packages/web/components/elements/Avatar.tsx index 50c47fc12..447dbb4b2 100644 --- a/packages/web/components/elements/Avatar.tsx +++ b/packages/web/components/elements/Avatar.tsx @@ -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%', }} > - {props.fallbackText} ) @@ -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', }) diff --git a/packages/web/components/elements/AvatarDropdown.tsx b/packages/web/components/elements/AvatarDropdown.tsx index f369d865a..ec24d0a1f 100644 --- a/packages/web/components/elements/AvatarDropdown.tsx +++ b/packages/web/components/elements/AvatarDropdown.tsx @@ -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 ( - + ) } diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 5c956ba29..43f7bc34c 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -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, + }, }, }, -}) \ No newline at end of file +}) diff --git a/packages/web/components/elements/CloseButton.tsx b/packages/web/components/elements/CloseButton.tsx new file mode 100644 index 000000000..439019de6 --- /dev/null +++ b/packages/web/components/elements/CloseButton.tsx @@ -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 ( + setHover(true)} + onMouseOut={() => setHover(false)} + > + + + ) +} diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index a3fb5e077..66dd5aa8f 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -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 ( <> - + { + props.onSelect() + }} + onClick={(event) => event.stopPropagation()} + > {props.title ?? props.children} ) } -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 ( - + {triggerElement} { + onInteractOutside={() => { // remove focus from dropdown ;(document.activeElement as HTMLElement).blur() }} diff --git a/packages/web/components/elements/ExtensionsInstallHelp.tsx b/packages/web/components/elements/ExtensionsInstallHelp.tsx index 2b10ddd11..5fc36447a 100644 --- a/packages/web/components/elements/ExtensionsInstallHelp.tsx +++ b/packages/web/components/elements/ExtensionsInstallHelp.tsx @@ -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': , diff --git a/packages/web/components/elements/HighlightNoteTextEditArea.tsx b/packages/web/components/elements/HighlightNoteTextEditArea.tsx new file mode 100644 index 000000000..7de604a19 --- /dev/null +++ b/packages/web/components/elements/HighlightNoteTextEditArea.tsx @@ -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): void => { + setNoteContent(event.target.value) + }, + [setNoteContent] + ) + + return ( + + + + + + + + ) +} diff --git a/packages/web/components/elements/InfoLink.tsx b/packages/web/components/elements/InfoLink.tsx index 5146f1bcf..44c0a7bc2 100644 --- a/packages/web/components/elements/InfoLink.tsx +++ b/packages/web/components/elements/InfoLink.tsx @@ -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' diff --git a/packages/web/components/elements/LabelChip.tsx b/packages/web/components/elements/LabelChip.tsx index c822e3eb1..67092c383 100644 --- a/packages/web/components/elements/LabelChip.tsx +++ b/packages/web/components/elements/LabelChip.tsx @@ -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 { {props.text} diff --git a/packages/web/components/elements/LabelColorDropdown.tsx b/packages/web/components/elements/LabelColorDropdown.tsx index 50690c247..763ee7289 100644 --- a/packages/web/components/elements/LabelColorDropdown.tsx +++ b/packages/web/components/elements/LabelColorDropdown.tsx @@ -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(false); + const iconColor = isDarkMode ? '#FFFFFF' : '#0A0806' + const [open, setOpen] = useState(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', }, }} > diff --git a/packages/web/components/elements/LogoBox.tsx b/packages/web/components/elements/LogoBox.tsx new file mode 100644 index 000000000..a0afa173d --- /dev/null +++ b/packages/web/components/elements/LogoBox.tsx @@ -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 ( + <> + + + + + + + + ) +} diff --git a/packages/web/components/elements/MenuTriggerButton.tsx b/packages/web/components/elements/MenuTriggerButton.tsx new file mode 100644 index 000000000..29a1700c9 --- /dev/null +++ b/packages/web/components/elements/MenuTriggerButton.tsx @@ -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 ( + setHover(true)} + onMouseOut={() => setHover(false)} + > + + {/* color="#ADADAD" /> + */} + + ) +} diff --git a/packages/web/components/elements/MobileInstallHelp.tsx b/packages/web/components/elements/MobileInstallHelp.tsx index b57ec2e05..926e6c287 100644 --- a/packages/web/components/elements/MobileInstallHelp.tsx +++ b/packages/web/components/elements/MobileInstallHelp.tsx @@ -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', diff --git a/packages/web/components/elements/ModalPrimitives.tsx b/packages/web/components/elements/ModalPrimitives.tsx index a9c0975c3..ad929112a 100644 --- a/packages/web/components/elements/ModalPrimitives.tsx +++ b/packages/web/components/elements/ModalPrimitives.tsx @@ -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) => { {props.title} - + + props.onOpenChange(false)} /> + ) } @@ -95,7 +90,7 @@ export const ModalButtonBar = (props: ModalButtonBarProps) => { }} > + ) : ( + + )} + + + + ) +} diff --git a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx new file mode 100644 index 000000000..21f031335 --- /dev/null +++ b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx @@ -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 ( + { + setIsHovered(true) + }} + onMouseLeave={() => { + setIsHovered(false) + }} + > + + + + + {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 ? ( + <> + {` • `} + + {`${Math.round(props.item.readingProgressPercent)}%`} + + + ) : null} + {props.item.highlights?.length ?? 0 > 0 + ? ` • ${props.item.highlights?.length} highlights` + : null} + + + setMenuOpen(open)} + actionHandler={props.handleAction} + triggerElement={ + + } + /> + + + + {props.item.title} + + {props.item.author} + {props.item.author && originText && ' | '} + + {originText} + + + + + + {props.item.labels?.map(({ name, color }, index) => ( + + ))} + + + + + + + ) +} diff --git a/packages/web/components/patterns/LibraryCards/LinkedItemCard.tsx b/packages/web/components/patterns/LibraryCards/LinkedItemCard.tsx index 28e4daf71..19caabc98 100644 --- a/packages/web/components/patterns/LibraryCards/LinkedItemCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LinkedItemCard.tsx @@ -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 - } if (props.layout == 'LIST_LAYOUT') { - return + return } else { - return + return } } diff --git a/packages/web/components/patterns/LibraryCards/ListLinkedItemCard.tsx b/packages/web/components/patterns/LibraryCards/ListLinkedItemCard.tsx deleted file mode 100644 index 3fa5ddfce..000000000 --- a/packages/web/components/patterns/LibraryCards/ListLinkedItemCard.tsx +++ /dev/null @@ -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 ( - } - largerLayoutNode={} - /> - ) -} - -export function ListLinkedItemCardNarrow( - props: LinkedItemCardProps -): JSX.Element { - return ( - { - props.handleAction('showDetail') - }} - > - - - - {props.item.title} - - - {props.item.author && ( - - {authoredByText(props.item.author)} - - )} - - {props.originText} - - - - - { - // This is here to prevent menu click events from bubbling - // up and causing us to "click" on the link item. - e.stopPropagation() - }} - > - - } - actionHandler={props.handleAction} - /> - - - ) -} - -export function ListLinkedItemCardWide( - props: LinkedItemCardProps -): JSX.Element { - return ( - { - props.handleAction('showDetail') - }} - > - - - {props.item.title} - - {props.item.author && ( - - {authoredByText(props.item.author)} - - )} - - {props.originText} - - - - - - { - // This is here to prevent menu click events from bubbling - // up and causing us to "click" on the link item. - e.stopPropagation() - }} - > - - } - actionHandler={props.handleAction} - /> - - - ) -} diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index cb8f65a81..9659158ac 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -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 ( <> - + - + + } actionHandler={props.actionHandler} /> @@ -241,7 +211,7 @@ function FloatingNavHeader(props: NavHeaderProps): JSX.Element { position: 'fixed', display: 'flex', alignItems: 'center', - zIndex: 100, + zIndex: 5, }} > @@ -256,16 +226,13 @@ function FloatingNavHeader(props: NavHeaderProps): JSX.Element { right: '18px', position: 'fixed', display: 'flex', - alignItems: 'center' + alignItems: 'center', }} > + } actionHandler={props.actionHandler} /> @@ -274,4 +241,3 @@ function FloatingNavHeader(props: NavHeaderProps): JSX.Element { ) } - diff --git a/packages/web/components/patterns/ReaderDropdownMenu.tsx b/packages/web/components/patterns/ReaderDropdownMenu.tsx new file mode 100644 index 000000000..8d75e0258 --- /dev/null +++ b/packages/web/components/patterns/ReaderDropdownMenu.tsx @@ -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 ( + + props.articleActionHandler('archive')} + title="Archive" + /> + props.articleActionHandler('editLabels')} + title="Edit Labels" + /> + props.articleActionHandler('showEditModal')} + title="Edit Info" + /> + props.articleActionHandler('delete')} + title="Delete" + /> + + window.Intercom('show')} + title="Feedback" + /> + + ) +} diff --git a/packages/web/components/patterns/ShareArticleView.tsx b/packages/web/components/patterns/ShareArticleView.tsx deleted file mode 100644 index c6c121492..000000000 --- a/packages/web/components/patterns/ShareArticleView.tsx +++ /dev/null @@ -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 ( - - {props.imageURL && ( - { - (e.target as HTMLElement).style.display = 'none' - }} - /> - )} - - - {props.title} - - - - - ) -} diff --git a/packages/web/components/templates/ArticleHighlights.tsx b/packages/web/components/templates/ArticleHighlights.tsx deleted file mode 100644 index ad79c7876..000000000 --- a/packages/web/components/templates/ArticleHighlights.tsx +++ /dev/null @@ -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 ( - - - - - - ) -} - -type LoadedContentProps = { - publicArticle: PublicArticleAttributes - showAllHighlights: boolean - selectedHighlightId?: string -} - -function LoadedContent(props: LoadedContentProps): JSX.Element { - const router = useRouter() - const container = useRef(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 ( - - {!props.selectedHighlightId && ( - - )} - - {selectedHighlights.map((highlight) => ( - - ))} - - {showAllHighlights && - unselectedHighlights.map((highlight) => ( - - ))} - - - - {!showAllHighlights && moreHighlightsCount > 0 && ( - - )} - - - - ) -} - -type LinkedItemProps = { - publicArticle: PublicArticleAttributes -} - -function LinkedItem(props: LinkedItemProps): JSX.Element { - const originText = new URL(props.publicArticle.url).hostname - - return ( - - - - - Link Preview Image - - - - - {props.publicArticle.title} - - {props.publicArticle.author && ( - - {authoredByText(props.publicArticle.author)} - - )} - - {originText} - - - - - - - {props.publicArticle.description} - - - ) -} diff --git a/packages/web/components/templates/KeyboardShortcutListModal.tsx b/packages/web/components/templates/KeyboardShortcutListModal.tsx index 9ba255933..a0f083cd9 100644 --- a/packages/web/components/templates/KeyboardShortcutListModal.tsx +++ b/packages/web/components/templates/KeyboardShortcutListModal.tsx @@ -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 ( @@ -72,10 +68,6 @@ export function KeyboardShortcutListModal( libraryListCommands(() => {}) )} /> - {})} - /> {})} diff --git a/packages/web/components/templates/LoginForm.tsx b/packages/web/components/templates/LoginForm.tsx index 2812c8428..c6f49e661 100644 --- a/packages/web/components/templates/LoginForm.tsx +++ b/packages/web/components/templates/LoginForm.tsx @@ -7,7 +7,6 @@ import { gauthRedirectURI, appleAuthRedirectURI, } from '../../lib/appConfig' -import AppleLogin from 'react-apple-login' import { AppleIdButton } from './auth/AppleIdButton' export type LoginFormProps = { diff --git a/packages/web/components/templates/LoginLayout.tsx b/packages/web/components/templates/LoginLayout.tsx index 552317723..4c6bbe82a 100644 --- a/packages/web/components/templates/LoginLayout.tsx +++ b/packages/web/components/templates/LoginLayout.tsx @@ -81,16 +81,12 @@ function MediumLoginLayout(props: LoginFormProps) { > - + ) } -type OmnivoreIllustrationProps = { - isLargeLayout?: boolean -} - -function OmnivoreIllustration({ isLargeLayout }: OmnivoreIllustrationProps) { +function OmnivoreIllustration() { return ( 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 ( + + ) + } + css={{ width: '240px' }} + > + + + + {viewerData.me && ( + <> + + {viewerData.me.name} + + + {`@${viewerData.me.profile.username}`} + + + )} + + + + {props.showThemeSection && } + headerDropdownActionHandler('navigate-to-install')} + title="Install" + /> + headerDropdownActionHandler('navigate-to-emails')} + title="Emails" + /> + headerDropdownActionHandler('navigate-to-labels')} + title="Labels" + /> + headerDropdownActionHandler('navigate-to-api')} + title="API Keys" + /> + headerDropdownActionHandler('navigate-to-integrations')} + title="Integrations" + /> + window.open('https://docs.omnivore.app', '_blank')} + title="Documentation" + /> + window.Intercom('show')} + title="Feedback" + /> + + headerDropdownActionHandler('logout')} + title="Logout" + /> + + ) +} + +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 ( + <> + + + + Mode + + + { + updateTheme(ThemeId.Light) + }} + > + Light + + + { + updateTheme(ThemeId.Darker) + }} + > + Dark + + + + + {props.layout && ( + + + Layout + + + { + props.updateLayout && props.updateLayout('LIST_LAYOUT') + }} + > + + + { + props.updateLayout && props.updateLayout('GRID_LAYOUT') + }} + > + + + + + )} + + + + ) +} diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index 30b652ea3..790dd7b2c 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -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 { 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 ? ( ) : null} - - + - {props.children} {showLogoutConfirmation ? ( { - 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 ( <> { 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' ? : <> } -type ActionDropdownProps = { - layout: ArticleActionsMenuLayout - triggerElement: JSX.Element - children: JSX.Element -} - -const ActionDropdown = (props: ActionDropdownProps): JSX.Element => { - return ( - - {props.children} - - ) -} - 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 && ( - <> - - - - )} @@ -138,36 +89,6 @@ export function ArticleActionsMenu( ) : ( - // - // - // - // } - // > - // { - // if (props.article?.id) { - // return setLabelsMutation( - // props.article?.id, - // labels.map((label) => label.id) - // ) - // } - // return Promise.resolve(undefined) - // }} - // onLabelsChanged={(labels) => { - // props.articleActionHandler('refreshLabels', labels) - // }} - // /> - // )} @@ -186,17 +110,22 @@ export function ArticleActionsMenu( onClick={() => props.articleActionHandler('setLabels')} css={{ display: 'none', - '@smDown': { + '@mdDown': { display: 'flex', + alignItems: 'center', }, }} > - + + + @@ -229,6 +184,10 @@ export function ArticleActionsMenu( @@ -249,7 +208,7 @@ export function ArticleActionsMenu( tooltipContent="Unarchive" tooltipSide={props.layout == 'side' ? 'right' : 'bottom'} > - + )} @@ -259,16 +218,6 @@ export function ArticleActionsMenu( */} - {readerSettings.showEditDisplaySettingsModal && ( - - readerSettings.setShowEditDisplaySettingsModal(false) - } - /> - )} {props.article && readerSettings.showSetLabelsModal && ( > } @@ -68,7 +73,7 @@ const RecommendationComments = ( - {props.recommendationsWithNotes.map((item, idx) => ( + {props.recommendationsWithNotes.map((item) => ( @@ -114,8 +118,8 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { const [fontFamilyOverride, setFontFamilyOverride] = useState( 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 { }} > + @@ -397,18 +406,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { onOpenChange={(open: boolean) => setShowReportIssuesModal(open)} /> ) : null} - {/* {showShareModal && ( - setShowShareModal(open)} - /> - )} */} ) } diff --git a/packages/web/components/templates/article/ArticleHeaderToolbar.tsx b/packages/web/components/templates/article/ArticleHeaderToolbar.tsx deleted file mode 100644 index f7ca4cf51..000000000 --- a/packages/web/components/templates/article/ArticleHeaderToolbar.tsx +++ /dev/null @@ -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> - 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 ( - - {props.hasHighlights && ( - - )} - {/* - {enablePostAction ? ( - - ) : null} - - {isLinkCopied ? ( - - Link Copied - - ) : null} */} - - ) -} diff --git a/packages/web/components/templates/article/DisplaySettingsModal.tsx b/packages/web/components/templates/article/DisplaySettingsModal.tsx index 80d506624..b0a3177d0 100644 --- a/packages/web/components/templates/article/DisplaySettingsModal.tsx +++ b/packages/web/components/templates/article/DisplaySettingsModal.tsx @@ -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 - 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 ( + { event.preventDefault() @@ -33,9 +37,7 @@ export function DisplaySettingsModal(props: DisplaySettingsModalProps): JSX.Elem }} > - + diff --git a/packages/web/components/templates/article/FontFamiliesOptions.tsx b/packages/web/components/templates/article/FontFamiliesOptions.tsx index ab0070916..c119d219c 100644 --- a/packages/web/components/templates/article/FontFamiliesOptions.tsx +++ b/packages/web/components/templates/article/FontFamiliesOptions.tsx @@ -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 ( - + props.onSelect(props.family)} > {props.family} {isSelected && ( - + )} ) @@ -47,22 +57,50 @@ function FontOption(props: FontOptionProps):JSX.Element { export function FontFamiliesOptions(props: FontFamiliesListProps): JSX.Element { return ( <> - - + + props.setShowFontFamilies(false)} > - - Choose Font + + + Choose Font + {FONT_FAMILIES.map((family) => ( - + ))} diff --git a/packages/web/components/templates/article/HighlightHoverCard.tsx b/packages/web/components/templates/article/HighlightHoverCard.tsx new file mode 100644 index 000000000..ccaa5a3cd --- /dev/null +++ b/packages/web/components/templates/article/HighlightHoverCard.tsx @@ -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 ( + + + + ) +} diff --git a/packages/web/components/templates/article/HighlightNoteModal.tsx b/packages/web/components/templates/article/HighlightNoteModal.tsx index 9c36fecf7..a2049506e 100644 --- a/packages/web/components/templates/article/HighlightNoteModal.tsx +++ b/packages/web/components/templates/article/HighlightNoteModal.tsx @@ -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): void => { setNoteContent(event.target.value) diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx index b88a2af4a..6ee2145eb 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -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(undefined) const [selectionData, setSelectionData] = useSelection(highlightLocations) - const [labelsTarget, setLabelsTarget] = useState( - undefined - ) + const [labelsTarget, setLabelsTarget] = + useState(undefined) const canShareNative = useCanShareNative() + const createHighlightFromSelection = async ( + selection: SelectionAttributes, + note?: string + ): Promise => { + 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 => { - 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 ( { @@ -615,23 +613,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { ) } - if ( - highlightModalAction?.highlightModalAction == 'share' && - highlightModalAction.highlight - ) { - return ( - { - 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)) { diff --git a/packages/web/components/templates/article/NotebookModal.tsx b/packages/web/components/templates/article/NotebookModal.tsx index 52a6c19eb..6dadb8030 100644 --- a/packages/web/components/templates/article/NotebookModal.tsx +++ b/packages/web/components/templates/article/NotebookModal.tsx @@ -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 ? ( setIsEditing(true)} > @@ -230,7 +225,7 @@ function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element { ) : null} {isEditing && ( - ) } - -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): void => { - setNoteContent(event.target.value) - }, - [setNoteContent] - ) - - return ( - - - - - - - - ) -} diff --git a/packages/web/components/templates/article/PdfArticleContainer.tsx b/packages/web/components/templates/article/PdfArticleContainer.tsx index 2449a0d7a..e297c1ded 100644 --- a/packages/web/components/templates/article/PdfArticleContainer.tsx +++ b/packages/web/components/templates/article/PdfArticleContainer.tsx @@ -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(null) - const [shareTarget, setShareTarget] = useState( - undefined - ) + const [shareTarget, setShareTarget] = + useState(undefined) const [notebookKey, setNotebookKey] = useState(uuidv4()) const [noteTarget, setNoteTarget] = useState(undefined) - const [noteTargetPageIndex, setNoteTargetPageIndex] = useState< - number | undefined - >(undefined) + const [noteTargetPageIndex, setNoteTargetPageIndex] = + useState(undefined) const highlightsRef = useRef([]) const canShareNative = useCanShareNative() @@ -465,17 +456,6 @@ export default function PdfArticleContainer( return (
- {shareTarget && ( - { - setShareTarget(undefined) - }} - /> - )} {noteTarget && ( 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 ( - - {showFontOptions ? ( - { - readerSettings.setFontFamily(font) - props.articleActionHandler('setFontFamily', font) - }} + <> + {showAdvanced ? ( + ) : ( - <> - - - - - - setShowFontOptions(true)} - > - Font: - - {readerSettings.fontFamily} - - - - - - - Margin: - - - - { - readerSettings.setMarginWidth(value) - props.articleActionHandler('setMarginWidth', value) - }} - /> - - - - - - - - Line Spacing: - - - - { - readerSettings.setLineHeight(value) - props.articleActionHandler('setLineHeight', value) - }} - /> - - - - - - - + )} + + ) +} + +function AdvancedSettings(props: SettingsProps): JSX.Element { + return ( + + + + + + { + props.readerSettings.setJustifyText(checked) + }} + > + + + + + + + { + props.readerSettings.setHighContrastText(checked) + }} + > + + + + + ) +} + +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 ( + + + + + + + + + + + + + + + + + + + ) +} + +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 ( + + + Font + ) => { + const font = e.currentTarget.value + if (FONT_FAMILIES.indexOf(font) < 0) { + return + } + props.readerSettings.setFontFamily(font) + }} + > + {FONT_FAMILIES.map((family) => ( + + ))} + + + + + + + + + ) +} + +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 ( + <> + + + Margin + + + + + + + + + + + Line Height + + + + { + props.readerSettings.setLineHeight(value) + }} + /> + + + + + ) +} + +function ThemeSelector(props: ReaderSettingsProps): JSX.Element { + const [currentTheme, setCurrentTheme] = useState(currentThemeName()) + + const isDark = useMemo(() => { + return currentTheme === 'Dark' || currentTheme === 'Darker' + }, [currentTheme]) + + return ( + + Themes + + + + ) } diff --git a/packages/web/components/templates/article/SetLabelsControl.tsx b/packages/web/components/templates/article/SetLabelsControl.tsx index e8b71cb9f..4fc12ee5d 100644 --- a/packages/web/components/templates/article/SetLabelsControl.tsx +++ b/packages/web/components/templates/article/SetLabelsControl.tsx @@ -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( 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) => { 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 ( { @@ -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 ( diff --git a/packages/web/components/templates/article/ShareArticleModal.tsx b/packages/web/components/templates/article/ShareArticleModal.tsx deleted file mode 100644 index 6481540ea..000000000 --- a/packages/web/components/templates/article/ShareArticleModal.tsx +++ /dev/null @@ -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 ( - - - - ) -} diff --git a/packages/web/components/templates/article/ShareHighlightModal.tsx b/packages/web/components/templates/article/ShareHighlightModal.tsx deleted file mode 100644 index b07433fab..000000000 --- a/packages/web/components/templates/article/ShareHighlightModal.tsx +++ /dev/null @@ -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 ( - - - - ) -} diff --git a/packages/web/components/templates/article/SkeletonArticleContainer.tsx b/packages/web/components/templates/article/SkeletonArticleContainer.tsx index d7ef809e7..73ea03830 100644 --- a/packages/web/components/templates/article/SkeletonArticleContainer.tsx +++ b/packages/web/components/templates/article/SkeletonArticleContainer.tsx @@ -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, }, }} > diff --git a/packages/web/components/templates/article/SnoozeLinkModal.tsx b/packages/web/components/templates/article/SnoozeLinkModal.tsx deleted file mode 100644 index 68167820e..000000000 --- a/packages/web/components/templates/article/SnoozeLinkModal.tsx +++ /dev/null @@ -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 ( - ) -} - -export function SnoozeLinkModal( - props: ShareArticleModalProps -): JSX.Element { - const [sendReminder, setSendReminder] = useState(false) - const [snoozeOption, setSnoozeOption] = useState(undefined) - - const setOption = (option: string) => { - setSnoozeOption(option) - setSendReminder(true) - }; - - return ( - - - { - event.preventDefault() - }} - css={{ - m: '0px', - p: '0px', - width: '375px', - height: '388px', - overflow: 'auto', - background: 'white' - }} - > - - - Snooze - - - - setOption('tonight')} /> - setOption('tomorrow')} /> - setOption('weekend')} /> - - - - { - setSendReminder(!sendReminder) - }} - /> - - - - - - - - - - ) -} diff --git a/packages/web/components/templates/article/VerticalArticleActions.tsx b/packages/web/components/templates/article/VerticalArticleActions.tsx new file mode 100644 index 000000000..cb04634ea --- /dev/null +++ b/packages/web/components/templates/article/VerticalArticleActions.tsx @@ -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 ( + <> + + + + + + + + + + {!props.article?.isArchived ? ( + + ) : ( + + )} + + + + } + articleActionHandler={props.articleActionHandler} + /> + + + ) +} diff --git a/packages/web/components/templates/auth/EmailLogin.tsx b/packages/web/components/templates/auth/EmailLogin.tsx index 8175f329f..0a4218b29 100644 --- a/packages/web/components/templates/auth/EmailLogin.tsx +++ b/packages/web/components/templates/auth/EmailLogin.tsx @@ -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(undefined) const [password, setPassword] = useState(undefined) - const [errorMessage, setErrorMessage] = - useState(undefined) + const [errorMessage, setErrorMessage] = useState( + undefined + ) useEffect(() => { if (!router.isReady) return diff --git a/packages/web/components/templates/auth/EmailSignup.tsx b/packages/web/components/templates/auth/EmailSignup.tsx index de55dc2c2..cdad620b8 100644 --- a/packages/web/components/templates/auth/EmailSignup.tsx +++ b/packages/web/components/templates/auth/EmailSignup.tsx @@ -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(undefined) const [fullname, setFullname] = useState(undefined) const [username, setUsername] = useState(undefined) - const [debouncedUsername, setDebouncedUsername] = - useState(undefined) - const [errorMessage, setErrorMessage] = - useState(undefined) + const [debouncedUsername, setDebouncedUsername] = useState< + string | undefined + >(undefined) + const [errorMessage, setErrorMessage] = useState( + 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 { diff --git a/packages/web/components/templates/auth/ResetSent.tsx b/packages/web/components/templates/auth/ResetSent.tsx index 8c95ce973..3f0f2776a 100644 --- a/packages/web/components/templates/auth/ResetSent.tsx +++ b/packages/web/components/templates/auth/ResetSent.tsx @@ -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 ( <> { - 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) => ( - - Link Saved - - - - ), { 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( + () => ( + + Link Saved + + + + ), + { 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 ( @@ -110,7 +115,10 @@ export function AddLinkModal(props: AddLinkModalProps): JSX.Element { fontSize: '14px', }} /> - + diff --git a/packages/web/components/templates/homeFeed/EditItemModals.tsx b/packages/web/components/templates/homeFeed/EditItemModals.tsx new file mode 100644 index 000000000..2920a7944 --- /dev/null +++ b/packages/web/components/templates/homeFeed/EditItemModals.tsx @@ -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 +} + +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 ( + + ) +} + +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 ( + + ) +} + +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 ( + + + { + // remove focus from modal + ;(document.activeElement as HTMLElement).blur() + }} + > + +
+ +
{ + event.preventDefault() + }} + > + + + SAVED AT: + { + const dateStr = event.target.value + setSavedAt(dayjs(dateStr)) + }} + css={{ + ...inputStyle, + fontSize: '14px', + }} + /> + + + PUBLISHED AT + { + const dateStr = event.target.value + setPublishedAt(dayjs(dateStr)) + }} + css={{ + ...inputStyle, + fontSize: '14px', + }} + /> + + + TITLE + setTitle(event.target.value)} + onFocus={(event) => { + event.target.select() + }} + css={inputStyle} + /> + AUTHOR + setAuthor(event.target.value)} + onFocus={(event) => { + event.target.select() + }} + css={inputStyle} + /> + DESCRIPTION + setDescription(event.target.value)} + onFocus={(event) => { + event.target.select() + }} + maxLength={4000} + /> + + + + + +
+ + + + ) +} + +type HeaderProps = { + onOpenChange: (open: boolean) => void +} + +function Header(props: HeaderProps): JSX.Element { + return ( + + Edit Title & Description + + props.onOpenChange(false)} /> + + + ) +} diff --git a/packages/web/components/templates/homeFeed/EditTitleModal.tsx b/packages/web/components/templates/homeFeed/EditTitleModal.tsx deleted file mode 100644 index 6cab4a025..000000000 --- a/packages/web/components/templates/homeFeed/EditTitleModal.tsx +++ /dev/null @@ -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 -} - -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 ( - - - { - // remove focus from modal - ;(document.activeElement as HTMLElement).blur() - }} - > - - - - Edit Title and Description - - - - Title - -
{ - event.preventDefault() - }} - > - setTitle(event.target.value)} - css={{ - borderRadius: '8px', - border: '1px solid $grayTextContrast', - width: '100%', - p: '$2', - }} - /> - Author - setAuthor(event.target.value)} - css={{ - borderRadius: '8px', - border: '1px solid $grayTextContrast', - width: '100%', - p: '$2', - }} - /> - - Description - - - setDescription(event.target.value)} - maxLength={4000} - /> - - - - - - -
-
-
-
- ) -} diff --git a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx index e2395dac2..eadc62608 100644 --- a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx +++ b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx @@ -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 ( - + - - No results found. - + + No results found. + - - You can add a link or read more about Omnivore's advanced search. - - - + + You can add a link or read more about Omnivore's{' '} + advanced search. + + + ) } diff --git a/packages/web/components/templates/homeFeed/HeaderSpacer.tsx b/packages/web/components/templates/homeFeed/HeaderSpacer.tsx new file mode 100644 index 000000000..51d47210c --- /dev/null +++ b/packages/web/components/templates/homeFeed/HeaderSpacer.tsx @@ -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 ( + + ) +} diff --git a/packages/web/components/templates/homeFeed/HighlightItem.tsx b/packages/web/components/templates/homeFeed/HighlightItem.tsx new file mode 100644 index 000000000..dd2e3841d --- /dev/null +++ b/packages/web/components/templates/homeFeed/HighlightItem.tsx @@ -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) + const [labelsTarget, setLabelsTarget] = useState( + undefined + ) + const [, updateState] = useState({}) + + return ( + <> + setHover(true)} + onMouseLeave={() => setHover(false)} + > + + { + if (router && props.viewer) { + const dest = `/${props.viewer}/${props.item.slug}#${props.highlight.id}` + router.push(dest) + } + event.preventDefault() + }} + > + + {lines.map((line: string, index: number) => ( + + {line} + {index !== lines.length - 1 && ( + <> +
+
+ + )} +
+ ))} +
+
+ + + {props.highlight.labels?.map((label: Label, index: number) => ( + + ))} + + + {!isEditing && ( + setIsEditing(true)} + > + {props.highlight.annotation + ? props.highlight.annotation + : 'Add your notes...'} + + )} + {isEditing && ( + {}} + /> + )} +
+ + + +
+ {showConfirmDeleteHighlightId && ( + { + setShowConfirmDeleteHighlightId(undefined) + const result = await deleteHighlightMutation( + showConfirmDeleteHighlightId + ) + if (result) { + showSuccessToast('Highlight deleted') + } else { + showErrorToast('Error deleting highlight') + } + }} + onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)} + /> + )} + {labelsTarget && ( + { + 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 ( + + + + } + > + { + copyHighlight() + }} + title="Copy" + /> + { + props.setLabelsTarget(props.highlight) + }} + title="Labels" + /> + { + exportHighlight() + }} + title="Delete" + /> + + ) +} + +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') +} diff --git a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx new file mode 100644 index 000000000..0d0c72d76 --- /dev/null +++ b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx @@ -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 +} + +export function HighlightItemsLayout( + props: HighlightItemsLayoutProps +): JSX.Element { + const [currentItem, setCurrentItem] = useState( + 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 ( + <> + + + + + {/* + + */} + + {props.items.map((linkedItem) => ( + { + setCurrentItem(linkedItem) + event.preventDefault() + }} + > + {props.viewer && ( + + )} + + ))} + + {currentItem && ( + <> + + + + + )} + + + ) +} + +type HighlightTitleCardProps = { + item: LibraryItem + viewer: UserBasicData + selected: boolean +} + +function LibraryItemCard(props: HighlightTitleCardProps): JSX.Element { + return ( + <> + + + + + + + + ) +} + +function HighlightTitleCard(props: HighlightTitleCardProps): JSX.Element { + return ( + + + + + + {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 ? ( + <> + {` • `} + + {`${Math.round(props.item.node.readingProgressPercent)}%`} + + + ) : null} + + + + {props.item.node.title} + + + + + + + ) +} + +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 ( + + + + + HIGHLIGHTS + + + + + } + > + { + exportHighlights() + }} + title="Export" + /> + + + + {(props.item.node.highlights ?? []).map((highlight) => ( + + ))} + + + + ) +} + +type HighlightCountChipProps = { + count: number + selected: boolean +} + +function HighlightCountChip(props: HighlightCountChipProps): JSX.Element { + return ( + + {props.count} + + + ) +} diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index cd2fb681a..480c6fcdd 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -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 = { - 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([]) + const [mode, setMode] = useState('reads') const defaultQuery = { limit: 10, @@ -101,14 +81,6 @@ export function HomeFeedContainer(): JSX.Element { const gridContainerRef = useRef(null) - const [shareTarget, setShareTarget] = useState( - undefined - ) - - const [snoozeTarget, setSnoozeTarget] = useState( - undefined - ) - const [labelsTarget, setLabelsTarget] = useState( undefined ) @@ -122,14 +94,6 @@ export function HomeFeedContainer(): JSX.Element { const [queryInputs, setQueryInputs] = useState(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 } +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({ 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 ( + + { + console.log('searching with searchQuery: ', searchQuery) + props.applySearchQuery(searchQuery) + }} + showFilterMenu={showFilterMenu} + setShowFilterMenu={setShowFilterMenu} + /> + + { + console.log('searching with searchQuery: ', searchQuery) + props.applySearchQuery(searchQuery) + }} + showFilterMenu={showFilterMenu} + setShowFilterMenu={setShowFilterMenu} + /> + + {props.mode == 'highlights' && ( + + )} + + {props.mode == 'reads' && ( + + )} + + + ) +} + +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 ( <> {props.isValidating && props.items.length == 0 && } - - - Library - - - { - updateLayout('GRID_LAYOUT') - }} - > - - - { - updateLayout('LIST_LAYOUT') - }} - > - - - - - - - {viewerData?.me && ( - - {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 ( - - ) - })} - - )} { @@ -922,7 +820,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { {({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
{inDragOperation && uploadingFiles.length < 1 && ( @@ -981,85 +879,21 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { }} /> ) : ( - - {props.items.map((linkedItem) => ( - div': { - bg: '$grayBg', - }, - '&:focus': { - '> div': { - bg: '$grayBgActive', - }, - }, - '&:hover': { - '> div': { - bg: '$grayBgActive', - }, - }, - }} - > - {viewerData?.me && ( - { - 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) - } - }} - /> - )} - - ))} - + )} - {/* Temporary code */} - {/*
- Files: -
    - {uploadingFiles.map((fileName) => ( -
  • {fileName}
  • - ))} -
-
*/} - {/* Temporary code */} + {props.showAddLinkModal && ( props.setShowAddLinkModal(false)} /> )} {props.showEditTitleModal && ( - 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 && ( - { - if (props.shareTarget) { - const item = document.getElementById(props.shareTarget.node.id) - if (item) { - item.focus() - } - props.setShareTarget(undefined) - } - }} - /> - )} - {props.snoozeTarget && ( - { - 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 && ( - {props.linkToRemove?.node && viewerData?.me && ( + {props.linkToRemove?.node && props.viewer && ( {}} @@ -1232,3 +1001,121 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { ) } + +type LibraryItemsProps = { + items: LibraryItem[] + layout: LayoutType + viewer: UserBasicData | undefined + + gridContainerRef: React.RefObject + + 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 +} + +function LibraryItems(props: LibraryItemsProps): JSX.Element { + return ( + + {props.items.map((linkedItem) => ( + div': { + bg: '$thBackground3', + }, + '&:focus': { + '> div': { + bg: '$thBackgroundActive', + }, + }, + '&:hover': { + '> div': { + bg: '$thBackgroundActive', + }, + '> a': { + bg: '$thBackgroundActive', + }, + }, + }} + > + {props.viewer && ( + { + 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) + } + }} + /> + )} + + ))} + + ) +} diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx new file mode 100644 index 000000000..786efb148 --- /dev/null +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -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 ( + <> + + + + + + props.setShowAddLinkModal(true)} + /> + + {/* This spacer pushes library content to the right of + the fixed left side menu. */} + + + ) +} + +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 ( + + {items.map((item) => ( + + ))} + + + + ) +} + +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 ( + { + window.location.href = '/settings/subscriptions' + }} + > + {subscriptions.slice(0, viewAll ? undefined : 4).map((item) => { + return ( + + ) + })} + + + ) +} + +function Labels(props: LibraryFilterMenuProps): JSX.Element { + const { labels } = useGetLabelsQuery() + const [viewAll, setViewAll] = useState(false) + + return ( + { + window.location.href = '/settings/labels' + }} + > + {labels.slice(0, viewAll ? undefined : 4).map((item) => { + return + })} + + + ) +} + +type MenuPanelProps = { + title: string + children: ReactNode + editFunc?: () => void + editTitle?: string +} + +function MenuPanel(props: MenuPanelProps): JSX.Element { + return ( + + + + {props.title} + + + {props.editTitle && props.editFunc && ( + + + + } + > + { + if (props.editFunc) { + props.editFunc() + } + }} + /> + + )} + +
+ {props.children} + + ) +} + +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 ( + { + props.applySearchQuery(props.filterTerm) + props.setShowFilterMenu(false) + e.preventDefault() + }} + > + {props.text} + + ) +} + +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 ( + + + + { + 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) + } + }} + /> + + + ) +} + +type AddLinkButtonProps = { + showAddLinkModal: () => void +} + +function AddLinkButton(props: AddLinkButtonProps): JSX.Element { + const currentTheme = currentThemeName() + const isDark = currentTheme == 'Dark' + + return ( + <> + + + + + + ) +} + +type ViewAllButtonProps = { + state: boolean + setState: (state: boolean) => void +} + +function ViewAllButton(props: ViewAllButtonProps): JSX.Element { + return ( + + ) +} diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx new file mode 100644 index 000000000..b569ab809 --- /dev/null +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -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 ( + <> + + {/* These will display/hide depending on breakpoints */} + + + + + {/* This spacer is put in to push library content down + below the fixed header height. */} + + + ) +} + +function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element { + return ( + + + + + + ) +} + +function SmallHeaderLayout(props: LibraryHeaderProps): JSX.Element { + const [showInlineSearch, setShowInlineSearch] = useState(false) + + return ( + + {showInlineSearch ? ( + + + + + ) : ( + <> + + + + )} + + ) +} + +type MenuHeaderButtonProps = { + showFilterMenu: boolean + setShowFilterMenu: (show: boolean) => void +} + +export function MenuHeaderButton(props: MenuHeaderButtonProps): JSX.Element { + return ( + { + props.setShowFilterMenu(!props.showFilterMenu) + }} + > + + + + ) +} + +export type SearchBoxProps = { + searchTerm: string | undefined + applySearchQuery: (searchQuery: string) => void + + compact?: boolean + onClose?: () => void +} + +export function SearchBox(props: SearchBoxProps): JSX.Element { + const inputRef = useRef(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 ( + + + { + inputRef.current?.focus() + e.preventDefault() + }} + > + + +
{ + event.preventDefault() + props.applySearchQuery(searchTerm || '') + inputRef.current?.blur() + if (props.onClose) { + props.onClose() + } + }} + style={{ width: '100%' }} + > + { + 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() + } + }} + /> + + {searchTerm && searchTerm.length ? ( + + { + event.preventDefault() + setSearchTerm('') + props.applySearchQuery('') + inputRef.current?.blur() + }} + tabIndex={-1} + > + + + + ) : ( + + + requestAnimationFrame(() => inputRef?.current?.focus()) + } + tabIndex={-1} + > + / + + + )} +
+
+ ) +} + +type ControlButtonBoxProps = { + layout: LayoutType + updateLayout: (layout: LayoutType) => void + setShowInlineSearch?: (show: boolean) => void +} + +function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element { + return ( + <> + + + + + + + + {props.setShowInlineSearch && ( + + + + + )} + + ) +} diff --git a/packages/web/components/templates/homeFeed/LibrarySearchBar.tsx b/packages/web/components/templates/homeFeed/LibrarySearchBar.tsx index e9127e0cd..79fd5f83f 100644 --- a/packages/web/components/templates/homeFeed/LibrarySearchBar.tsx +++ b/packages/web/components/templates/homeFeed/LibrarySearchBar.tsx @@ -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 ( { return integrations.find((i) => i.type == 'READWISE') }, [integrations]) @@ -57,23 +59,23 @@ export function Readwise(): JSX.Element { {readwiseIntegration && ( - - )} - - {!readwiseIntegration && ( - + )} + {!readwiseIntegration && } ) } - function AddReadwiseForm(): JSX.Element { const router = useRouter() - const [errorMessage, setErrorMessage] = - useState(undefined) + const [errorMessage, setErrorMessage] = useState( + undefined + ) const [token, setToken] = useState('') const setReadwiseToken = useCallback(async () => { @@ -94,64 +96,64 @@ function AddReadwiseForm(): JSX.Element { } }, [token, router]) - return (<> - - Enter your API key from Readwise below. You can get your token{' '} - + - here - . - + + Enter your API key from Readwise below. You can get your token{' '} + + here + + . + + - { - 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 && {errorMessage}} - + { + 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 && {errorMessage}} + ) } @@ -179,19 +181,23 @@ function RemoveReadwiseForm(props: RemoveReadwiseFormProps): JSX.Element { }, [props]) return ( - <> - - Omnivore is configured to send all your highlights to Readwise. - + <> + + + Omnivore is configured to send all your highlights to Readwise. + + - - + + ) -} \ No newline at end of file +} diff --git a/packages/web/components/templates/landing/LandingFooter.tsx b/packages/web/components/templates/landing/LandingFooter.tsx index 5caea4a8a..1300ff284 100644 --- a/packages/web/components/templates/landing/LandingFooter.tsx +++ b/packages/web/components/templates/landing/LandingFooter.tsx @@ -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 = { diff --git a/packages/web/components/templates/library/LibraryAvatar.tsx b/packages/web/components/templates/library/LibraryAvatar.tsx deleted file mode 100644 index f986b53df..000000000 --- a/packages/web/components/templates/library/LibraryAvatar.tsx +++ /dev/null @@ -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 ( - - - - {props.viewer?.profile.pictureUrl - ? - : {props.viewer?.name.charAt(0) ?? ''} - } - - - {/* This spacer is to help align with items in the search box */} - - - ) -} - -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', -}) diff --git a/packages/web/components/templates/library/LibraryContainer.tsx b/packages/web/components/templates/library/LibraryContainer.tsx deleted file mode 100644 index 6a9f1825e..000000000 --- a/packages/web/components/templates/library/LibraryContainer.tsx +++ /dev/null @@ -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({ - key: 'libraryLayout', - initialValue: 'GRID_LAYOUT', - }) - - return { - layout, - setLayout - } -} - -export function LibraryContainer(): JSX.Element { - useGetUserPreferences() - - const { viewerData } = useGetViewerQuery() - const searchCoordinator = useSearchCoordinator() - const layoutCoordinator = useLibraryLayoutCoordinator() - - return ( - <> - - - - - - - - - - - - - - - - - - - ) -} \ No newline at end of file diff --git a/packages/web/components/templates/library/LibraryHeadline.tsx b/packages/web/components/templates/library/LibraryHeadline.tsx deleted file mode 100644 index afeee7a62..000000000 --- a/packages/web/components/templates/library/LibraryHeadline.tsx +++ /dev/null @@ -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 ( - - Home - - - - - - - ) -} \ No newline at end of file diff --git a/packages/web/components/templates/library/LibraryList.tsx b/packages/web/components/templates/library/LibraryList.tsx deleted file mode 100644 index ebea7d71a..000000000 --- a/packages/web/components/templates/library/LibraryList.tsx +++ /dev/null @@ -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 ( - { - console.log('onAddLinkClicked') - }} - /> - ) - } - console.log(fileNames) - - return ( - - {inDragOperation && uploadingFiles.length < 1 && ( - - - Drag n drop files here - - - )} - { - setInDragOperation(true) - }} - onDragLeave={() => { - setInDragOperation(false) - }} - noClick={true} - noDragEventsBubbling={true} - > - {({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => ( - - - - {libraryItems.map((linkedItem) => ( - div': { - bg: '$libraryBackground', - }, - '&:focus': { - '> div': { - bg: '$grayBgActive', - }, - }, - '&:hover': { - '> div': { - bg: '$grayBgActive', - }, - }, - }} - > - {viewerData?.me && ( - { - console.log('card clicked') - }} - /> - )} - - ))} - - - )} - - {/* Temporary code */} -
- Files: -
    - {fileNames.map((fileName) => ( -
  • {fileName}
  • - ))} -
-
{' '} - {/* Temporary code */} - {/* Extra padding at bottom to give space for scrolling */} - - - ) -} diff --git a/packages/web/components/templates/library/LibraryMenu.tsx b/packages/web/components/templates/library/LibraryMenu.tsx deleted file mode 100644 index b1ffaa9b6..000000000 --- a/packages/web/components/templates/library/LibraryMenu.tsx +++ /dev/null @@ -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 ( - - - - ) -} diff --git a/packages/web/components/templates/library/LibrarySearchBar.tsx b/packages/web/components/templates/library/LibrarySearchBar.tsx deleted file mode 100644 index fb3a499dc..000000000 --- a/packages/web/components/templates/library/LibrarySearchBar.tsx +++ /dev/null @@ -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()) - - useEffect(() => { - setRecentSearches(Object.values(localStorage)) - }, []) - - return ( - (item ? item : '')}> - {({ - getInputProps, - getRootProps, - getMenuProps, - getItemProps, - isOpen, - highlightedIndex, - inputValue, - clearSelection, - openMenu, - }) => ( - - -
{ - event.preventDefault() - // props.applySearchQuery(searchTerm || '') - // inputRef.current?.blur() - }} - {...getRootProps()} - > - { - event.preventDefault() - openMenu() - //props.applySearchQuery('') - // inputRef.current?.blur() - }} - onChange={(event: any) => { - event.preventDefault() - }} - {...getInputProps()} - /> - - {/* {searchTerm && ( */} - - - - - {/* )} */} - {/* {!searchTerm && ( */} - - {/* )} */} - - - - {isOpen && - recentSearches - .filter((item) => !inputValue || item.includes(inputValue)) - .map((item, index) => ( - - - - {item} - - { - localStorage.removeItem(`${item}`) - setRecentSearches(Object.values(localStorage)) - }} - /> - - ))} - - -
-
- )} -
- ) -} diff --git a/packages/web/components/templates/onboarding/OnboardingAddNewsletters.tsx b/packages/web/components/templates/onboarding/OnboardingAddNewsletters.tsx deleted file mode 100644 index ea472bd40..000000000 --- a/packages/web/components/templates/onboarding/OnboardingAddNewsletters.tsx +++ /dev/null @@ -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(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 ( - - - {newsletters.map(({ icon, label, description, isChecked }, idx) => ( - - ))} - - - ) -} - diff --git a/packages/web/components/templates/onboarding/OnboardingHighlightInstructions.tsx b/packages/web/components/templates/onboarding/OnboardingHighlightInstructions.tsx deleted file mode 100644 index fbf3614a9..000000000 --- a/packages/web/components/templates/onboarding/OnboardingHighlightInstructions.tsx +++ /dev/null @@ -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 ( - - - - - - ) -} diff --git a/packages/web/components/templates/onboarding/OnboardingInstallInstructions.tsx b/packages/web/components/templates/onboarding/OnboardingInstallInstructions.tsx deleted file mode 100644 index 301e4d821..000000000 --- a/packages/web/components/templates/onboarding/OnboardingInstallInstructions.tsx +++ /dev/null @@ -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 ( - - } - > - - - - - - - - - - - - Email me instructions - - - - - ) -} - diff --git a/packages/web/components/templates/onboarding/OnboardingJoinCommunity.tsx b/packages/web/components/templates/onboarding/OnboardingJoinCommunity.tsx deleted file mode 100644 index c85711f43..000000000 --- a/packages/web/components/templates/onboarding/OnboardingJoinCommunity.tsx +++ /dev/null @@ -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 ( - - - - - - - - - Star us on Github - - - - - - Join us on Discord - - - - - - Like us on Product Hunt - - - - - - ) -} - diff --git a/packages/web/components/templates/onboarding/OnboardingOrganizeInstructions.tsx b/packages/web/components/templates/onboarding/OnboardingOrganizeInstructions.tsx deleted file mode 100644 index 3704732c2..000000000 --- a/packages/web/components/templates/onboarding/OnboardingOrganizeInstructions.tsx +++ /dev/null @@ -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 ( - - - - - - - ) -} diff --git a/packages/web/components/templates/onboarding/OnboardingReaderPreview.tsx b/packages/web/components/templates/onboarding/OnboardingReaderPreview.tsx deleted file mode 100644 index fac558917..000000000 --- a/packages/web/components/templates/onboarding/OnboardingReaderPreview.tsx +++ /dev/null @@ -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 ( - - } - > - - - - {articleDetails.map(({ title, author, originText, description, image, labels }, idx) => ( - - ))} - - - - - ) -} diff --git a/packages/web/components/templates/onboarding/SelectOption.tsx b/packages/web/components/templates/onboarding/SelectOption.tsx deleted file mode 100644 index 3bd794f44..000000000 --- a/packages/web/components/templates/onboarding/SelectOption.tsx +++ /dev/null @@ -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 ( - - undefined} /> - - {`${icon.slice(0, - - - - {label} - - - {description} - - - - ) -} - -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 ( - - div': { - borderRadius: '100vmax 100vmax 0 0', - }, - }} - > - - - - - - - - - { - // This is here to prevent menu click events from bubbling - // up and causing us to "click" on the link item. - e.stopPropagation() - }} - > - - - - - {author && ( - - {authoredByText(author)} - - )} - - {originText} - - - - - - - {description} - - {image && ( - { - ;(e.target as HTMLElement).style.display = 'none' - }} - /> - )} - - - {labels?.map(({ name, color }, index) => ( - - ))} - - - ) -} - -type CardTitleProps = { - title: string -} - -function CardTitle(props: CardTitleProps): JSX.Element { - return ( - - {props.title} - - ) -} diff --git a/packages/web/components/templates/reader/ReaderHeader.tsx b/packages/web/components/templates/reader/ReaderHeader.tsx new file mode 100644 index 000000000..e14e3a46c --- /dev/null +++ b/packages/web/components/templates/reader/ReaderHeader.tsx @@ -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 ( + <> + + + + + {props.children} + + {!props.alwaysDisplayToolbar && ( + + + + )} + + + + ) +} + +function ControlButtonBox(props: ReaderHeaderProps): JSX.Element { + return ( + <> + + + + + + + + ) +} diff --git a/packages/web/components/templates/settings/SettingsTable.tsx b/packages/web/components/templates/settings/SettingsTable.tsx index ed63058f1..d57c74955 100644 --- a/packages/web/components/templates/settings/SettingsTable.tsx +++ b/packages/web/components/templates/settings/SettingsTable.tsx @@ -1,5 +1,4 @@ -import Link from 'next/link' -import { Plus, Trash } from 'phosphor-react' +import { Trash } from 'phosphor-react' import { Toaster } from 'react-hot-toast' import { Button } from '../../elements/Button' import { Dropdown, DropdownOption } from '../../elements/DropdownElements' @@ -7,8 +6,8 @@ import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon' import { InfoLink } from '../../elements/InfoLink' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { StyledText } from '../../elements/StyledText' -import { styled, theme } from '../../tokens/stitches.config' -import { PrimaryLayout } from '../PrimaryLayout' +import { theme } from '../../tokens/stitches.config' +import { SettingsLayout } from '../SettingsLayout' type SettingsTableProps = { pageId: string @@ -236,7 +235,7 @@ const CreateButton = (props: CreateButtonProps): JSX.Element => { export const SettingsTable = (props: SettingsTableProps): JSX.Element => { return ( - + { - + ) } diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index d3f42cbcc..019904201 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -66,6 +66,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = theme: { fonts: { inter: 'Inter, sans-serif', + display: '-apple-system, BlinkMacSystemFont, sans-serif', }, fontSizes: { 1: '0.75em', @@ -108,8 +109,12 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = }, zIndices: {}, transitions: {}, + + colorScheme: { + colorScheme: 'light', + }, + colors: { - // Grayscale grayBase: '#F8F8F8', grayBg: '#FFFFFF', grayBgActive: '#e6e6e6', @@ -126,7 +131,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = grayBgHover: 'hsl(0 0% 93.0%)', grayLine: 'hsl(0 0% 88.7%)', grayBorderHover: 'hsl(0 0% 78.0%)', - grayText: '#3B3938', + grayText: '#6A6968', graySeparator: '#DADADA', grayProgressBackground: '#FFFFFF', @@ -155,8 +160,8 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = readerTableHeader: '#FFFFFF', // Avatar Fallback color - avatarBg: '#FFFFFF', - avatarFont: '#0A0806', + avatarBg: '#FFEA9F', + avatarFont: '#9C7C0A', labelButtonsBg: '#F5F5F4', tooltipIcons: '#FDFAEC', @@ -169,6 +174,28 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = //utility textNonEssential: 'rgba(10, 8, 6, 0.4)', overlay: 'rgba(63, 62, 60, 0.2)', + + // New theme, special naming to keep things straigh + // once all switch over, we will rename + thBackground: '#FFFFFF', + thBackground2: '#F3F3F3', + thBackground3: '#FFFFFF', + thBackground4: '#EBEBEB', + thBackgroundContrast: '#FFFFFF', + + thTextContrast: '#1E1E1E', + thTextContrast2: '#3D3D3D', + + thTextSubtle: '#1E1E1E', + thTextSubtle2: '#6A6968', + thTextSubtle3: '#ADADAD', + + thBorderColor: '#E1E1E1', + thBorderSubtle: '#EEEEEE', + + thProgressFg: '#FFD234', + + thHighContrast: '#3D3D3D', }, }, media: { @@ -176,14 +203,19 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = smDown: '(max-width: 575px)', mdDown: '(max-width: 768px)', lgDown: '(max-width: 992px)', + xlgDown: '(max-width: 1200px)', sm: '(min-width: 576px)', md: '(min-width: 768px)', lg: '(min-width: 992px)', xl: '(min-width: 1200px)', + xxl: '(min-width: 1700px)', }, }) const darkThemeSpec = { + colorScheme: { + colorScheme: 'dark', + }, colors: { grayBase: '#252525', grayBg: '#3B3938', @@ -217,8 +249,8 @@ const darkThemeSpec = { readerHeader: '#b9b9b9', readerTableHeader: '#FFFFFF', tooltipIcons: '#5F5E58', - avatarBg: '#000000', - avatarFont: 'rgba(255, 255, 255, 0.8)', + avatarBg: '#7B5C3E', + avatarFont: '#D9D9D9', textSubtle: '#AAAAAA', libraryBackground: '#252525', @@ -231,6 +263,31 @@ const darkThemeSpec = { overlay: 'rgba(10, 8, 6, 0.65)', labelButtonsBg: '#5F5E58', + + // New theme, special naming to keep things straigh + // once all switch over, we will rename + // DARK + colorScheme: 'dark', + thBackground: '#2A2A2A', + thBackground2: '#3D3D3D', + thBackground3: '#242424', + thBackground4: '#3D3D3D', + thBackgroundActive: '#2A2A2B', + thBackgroundContrast: '#000000', + + thTextContrast: '#FFFFFF', + thTextContrast2: '#EBEBEB', + + thTextSubtle: '#D9D9D9', + thTextSubtle2: '#D9D9D9', + thTextSubtle3: '#ADADAD', + + thBorderColor: '#4F4F4F', + thBorderSubtle: '#6A6968', + + thProgressFg: '#FFEA9F', + + thHighContrast: '#D9D9D9', }, shadows: { cardBoxShadow: @@ -280,7 +337,8 @@ export const lighterTheme = createTheme(ThemeId.Lighter, {}) // Apply global styles in here export const globalStyles = globalCss({ body: { - backgroundColor: '$grayBase', + colorScheme: 'var(--colorScheme-colorScheme)', + backgroundColor: '$thBackground', }, '*': { '&:focus': { diff --git a/packages/web/lib/articleActions.ts b/packages/web/lib/articleActions.ts index f599d1bfc..8feaa4f61 100644 --- a/packages/web/lib/articleActions.ts +++ b/packages/web/lib/articleActions.ts @@ -1,14 +1,21 @@ -import { Highlight } from "./networking/fragments/highlightFragment" -import { ArticleReadingProgressMutationInput } from "./networking/mutations/articleReadingProgressMutation" -import { CreateHighlightInput } from "./networking/mutations/createHighlightMutation" -import { MergeHighlightInput, MergeHighlightOutput } from "./networking/mutations/mergeHighlightMutation" -import { UpdateHighlightInput } from "./networking/mutations/updateHighlightMutation" - +import { Highlight } from './networking/fragments/highlightFragment' +import { ArticleReadingProgressMutationInput } from './networking/mutations/articleReadingProgressMutation' +import { CreateHighlightInput } from './networking/mutations/createHighlightMutation' +import { MergeHighlightInput } from './networking/mutations/mergeHighlightMutation' +import { UpdateHighlightInput } from './networking/mutations/updateHighlightMutation' export type ArticleMutations = { - createHighlightMutation: (input: CreateHighlightInput) => Promise + createHighlightMutation: ( + input: CreateHighlightInput + ) => Promise deleteHighlightMutation: (highlightId: string) => Promise - mergeHighlightMutation: (input: MergeHighlightInput) => Promise - updateHighlightMutation: (input: UpdateHighlightInput) => Promise - articleReadingProgressMutation: (input: ArticleReadingProgressMutationInput) => Promise + mergeHighlightMutation: ( + input: MergeHighlightInput + ) => Promise + updateHighlightMutation: ( + input: UpdateHighlightInput + ) => Promise + articleReadingProgressMutation: ( + input: ArticleReadingProgressMutationInput + ) => Promise } diff --git a/packages/web/lib/dateFormatting.ts b/packages/web/lib/dateFormatting.ts index 93be4d330..587db2e88 100644 --- a/packages/web/lib/dateFormatting.ts +++ b/packages/web/lib/dateFormatting.ts @@ -13,48 +13,3 @@ export function formattedShortDate(rawDate: string): string { dateStyle: 'short', }).format(new Date(rawDate)) } - -export function readableUpdatedAtMessage( - rawDate: string, - customPrefix?: string -): string { - const prefix = customPrefix || 'Updated ' - const timeElapsed = Math.ceil( - new Date().valueOf() - new Date(rawDate).valueOf() - ) - const secondsElapsed = timeElapsed / 1000 - - if (secondsElapsed < 60) { - return `${prefix} a few seconds ago` - } - - if (secondsElapsed < 3600) { - return `${prefix} ${Math.floor(secondsElapsed / 60)} minutes ago` - } - - if (secondsElapsed < 86400) { - return `${prefix} ${Math.floor(secondsElapsed / 3600)} hours ago` - } - - if (secondsElapsed < 604800) { - return `${prefix} ${Math.floor(secondsElapsed / 86400)} days ago` - } - - if (secondsElapsed < 2592000) { - return `${prefix} ${Math.floor(secondsElapsed / 604800)} weeks ago` - } - - if (secondsElapsed < 31536000) { - return `${prefix} ${Math.floor(secondsElapsed / 2592000)} months ago` - } - - if (secondsElapsed < 315360000) { - return `${prefix} ${Math.floor(secondsElapsed / 31536000)} years ago` - } - - if (secondsElapsed < 3153600000) { - return `${prefix} ${Math.floor(secondsElapsed / 315360000)} decades ago` - } - - return '' -} diff --git a/packages/web/lib/highlights/highlightGenerator.ts b/packages/web/lib/highlights/highlightGenerator.ts index 0175d717b..07027d743 100644 --- a/packages/web/lib/highlights/highlightGenerator.ts +++ b/packages/web/lib/highlights/highlightGenerator.ts @@ -468,19 +468,6 @@ const getSurroundingText = ({ } } -function stringToColour(str: string): string { - let hash = 0 - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash) - } - let colour = '#' - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xff - colour += ('00' + value.toString(16)).substr(-2) - } - return colour -} - export const isValidLength = (patch: string): boolean => { const { highlightTextStart, highlightTextEnd } = getPrefixAndSuffix({ patch }) return highlightTextEnd - highlightTextStart < maxHighlightLength diff --git a/packages/web/lib/hooks/useReaderSettings.tsx b/packages/web/lib/hooks/useReaderSettings.tsx index 11c3c5fdf..af2293bd3 100644 --- a/packages/web/lib/hooks/useReaderSettings.tsx +++ b/packages/web/lib/hooks/useReaderSettings.tsx @@ -1,8 +1,11 @@ -import { useRegisterActions } from "kbar" -import { useCallback, useState } from "react" -import { userPersonalizationMutation } from "../networking/mutations/userPersonalizationMutation" -import { useGetUserPreferences, UserPreferences } from "../networking/queries/useGetUserPreferences" -import { usePersistedState } from "./usePersistedState" +import { useRegisterActions } from 'kbar' +import { useCallback, useState } from 'react' +import { userPersonalizationMutation } from '../networking/mutations/userPersonalizationMutation' +import { + useGetUserPreferences, + UserPreferences, +} from '../networking/queries/useGetUserPreferences' +import { usePersistedState } from './usePersistedState' const DEFAULT_FONT = 'Inter' @@ -14,7 +17,7 @@ export type ReaderSettings = { setFontSize: (newFontSize: number) => void setLineHeight: (newLineHeight: number) => void - setMarginWidth: (newMarginWidth: number) => void + setMarginWidth: (newMarginWidth: number) => void showSetLabelsModal: boolean showDeleteConfirmation: boolean @@ -22,125 +25,207 @@ export type ReaderSettings = { setShowSetLabelsModal: (showSetLabelsModal: boolean) => void setShowDeleteConfirmation: (showDeleteConfirmation: boolean) => void - setShowEditDisplaySettingsModal: (showEditDisplaySettingsModal: boolean) => void + setShowEditDisplaySettingsModal: ( + showEditDisplaySettingsModal: boolean + ) => void actionHandler: (action: string, arg?: unknown) => void - - fontFamily: string, + + fontFamily: string setFontFamily: (newStyle: string) => void + + justifyText: boolean | undefined + setJustifyText: (set: boolean) => void + highContrastText: boolean | undefined + setHighContrastText: (set: boolean) => void } export const useReaderSettings = (): ReaderSettings => { 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 [fontFamily, setFontFamily] = usePersistedState({ key: 'fontFamily', initialValue: DEFAULT_FONT }) + const [, updateState] = useState({}) + + const [fontSize, setFontSize] = usePersistedState({ + key: 'fontSize', + initialValue: preferencesData?.fontSize ?? 20, + }) + const [lineHeight, setLineHeight] = usePersistedState({ + key: 'lineHeight', + initialValue: 150, + }) + const [marginWidth, setMarginWidth] = usePersistedState({ + key: 'marginWidth', + initialValue: 200, + }) + const [fontFamily, setFontFamily] = usePersistedState({ + key: 'fontFamily', + initialValue: DEFAULT_FONT, + }) + const [highContrastText, setHighContrastText] = usePersistedState< + boolean | undefined + >({ + key: `--display-high-contrast-text`, + initialValue: false, + }) + + const [justifyText, setJustifyText] = usePersistedState({ + key: `--display-justify-text`, + initialValue: false, + }) const [showSetLabelsModal, setShowSetLabelsModal] = useState(false) - const [showEditDisplaySettingsModal, setShowEditDisplaySettingsModal] = useState(false) + const [showEditDisplaySettingsModal, setShowEditDisplaySettingsModal] = + useState(false) const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false) const updateFontSize = async (newFontSize: number) => { setFontSize(newFontSize) - await userPersonalizationMutation({ fontSize: newFontSize }) + ;(async () => { + await userPersonalizationMutation({ fontSize: newFontSize }) + })() } - const actionHandler = useCallback(async(action: string, arg?: unknown) => { - switch (action) { - 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) + // const [hideMargins, setHideMargins] = usePersistedState({ + // key: `--display-hide-margins`, + // initialValue: false, + // isSessionStorage: false, + // }) + + const actionHandler = useCallback( + (action: string, arg?: unknown) => { + switch (action) { + case 'setFontSize': + const value = Number(arg) + if (value >= 10 && value <= 34) { + updateFontSize(value) + } + break + case 'incrementFontSize': + updateFontSize(Math.min(fontSize + 2, 34)) + break + case 'decrementFontSize': + updateFontSize(Math.max(fontSize - 2, 10)) + break + case 'setMarginWidth': { + const value = Number(arg) + console.log('setMarginWidth: ', value) + if (value >= 200 && value <= 560) { + setMarginWidth(value) + } + break } - 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) + 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 'setFontFamily': { + setFontFamily(arg as unknown as string) + break + } + case 'setLabels': { + setShowSetLabelsModal(true) + break + } + case 'resetReaderSettings': { + updateFontSize(20) + setMarginWidth(290) + setLineHeight(150) + setFontFamily(DEFAULT_FONT) + break } - break } - case 'editDisplaySettings': { - setShowEditDisplaySettingsModal(true) - break - } - case 'setFontFamily': { - setFontFamily(arg as unknown as string) - break - } - case 'setLabels': { - setShowSetLabelsModal(true) - break - } - case 'resetReaderSettings': { - updateFontSize(20) - setMarginWidth(290) - setLineHeight(150) - setFontFamily(DEFAULT_FONT) - break - } - } - }, [fontSize, setFontSize, lineHeight, fontFamily, - setLineHeight, marginWidth, setMarginWidth, setFontFamily]) - - - useRegisterActions([ - { - id: 'increaseFont', - section: 'Article', - name: 'Increase font size', - shortcut: ['+'], - perform: () => actionHandler('incrementFontSize'), }, - { - id: 'decreaseFont', - section: 'Article', - name: 'Decrease font size', - shortcut: ['-'], - perform: () => actionHandler('decrementFontSize'), - }, - { - id: 'increaseMargin', - section: 'Article', - name: 'Increase margin width', - shortcut: [']'], - perform: () => actionHandler('incrementMarginWidth'), - }, - { - id: 'decreaseMargin', - section: 'Article', - name: 'Decrease margin width', - shortcut: ['['], - perform: () => actionHandler('decrementMarginWidth'), - }, - { - id: 'edit_a', - section: 'Article', - name: 'Edit labels', - shortcut: ['l'], - perform: () => setShowSetLabelsModal(true), - }, - ], []) + [ + fontSize, + setFontSize, + lineHeight, + fontFamily, + setLineHeight, + marginWidth, + setMarginWidth, + setFontFamily, + ] + ) + + useRegisterActions( + [ + { + id: 'increaseFont', + section: 'Article', + name: 'Increase font size', + shortcut: ['+'], + perform: () => actionHandler('incrementFontSize'), + }, + { + id: 'decreaseFont', + section: 'Article', + name: 'Decrease font size', + shortcut: ['-'], + perform: () => actionHandler('decrementFontSize'), + }, + { + id: 'increaseMargin', + section: 'Article', + name: 'Increase margin width', + shortcut: [']'], + perform: () => actionHandler('incrementMarginWidth'), + }, + { + id: 'decreaseMargin', + section: 'Article', + name: 'Decrease margin width', + shortcut: ['['], + perform: () => actionHandler('decrementMarginWidth'), + }, + { + id: 'edit_labels', + section: 'Article', + name: 'Edit labels', + shortcut: ['l'], + perform: () => setShowSetLabelsModal(true), + }, + { + id: 'display_settings', + section: 'Article', + name: 'Display settings', + shortcut: ['d'], + perform: () => setShowEditDisplaySettingsModal(true), + }, + ], + [actionHandler] + ) return { preferencesData, - fontSize, lineHeight, marginWidth, - setFontSize, setLineHeight, setMarginWidth, - showDeleteConfirmation, showSetLabelsModal, showEditDisplaySettingsModal, - setShowSetLabelsModal, setShowEditDisplaySettingsModal, setShowDeleteConfirmation, - actionHandler, setFontFamily, fontFamily, + fontSize, + lineHeight, + marginWidth, + setFontSize, + setLineHeight, + setMarginWidth, + showDeleteConfirmation, + showSetLabelsModal, + showEditDisplaySettingsModal, + setShowSetLabelsModal, + setShowEditDisplaySettingsModal, + setShowDeleteConfirmation, + actionHandler, + setFontFamily, + fontFamily, + justifyText, + setJustifyText, + highContrastText, + setHighContrastText, } } diff --git a/packages/web/lib/hooks/useScrollWatcher.tsx b/packages/web/lib/hooks/useScrollWatcher.tsx index fb65894e2..19580ed43 100644 --- a/packages/web/lib/hooks/useScrollWatcher.tsx +++ b/packages/web/lib/hooks/useScrollWatcher.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useState, useCallback } from 'react' +import { useRef, useEffect, useState } from 'react' type ScrollOffset = { x: number @@ -12,10 +12,7 @@ export type ScrollOffsetChangeset = { type Effect = (offset: ScrollOffsetChangeset) => void -export function useScrollWatcher( - effect: Effect, - delay: number -): void { +export function useScrollWatcher(effect: Effect, delay: number): void { const throttleTimeout = useRef(undefined) const [currentOffset, setCurrentOffset] = useState({ x: 0, @@ -40,7 +37,6 @@ export function useScrollWatcher( } window.addEventListener('scroll', handleScroll) - return () => - window.removeEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) }, [currentOffset, delay, effect]) } diff --git a/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts b/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts index 0bc1c841f..2c4bd0bab 100644 --- a/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts +++ b/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts @@ -183,7 +183,7 @@ export function libraryListCommands( actionDescription: 'Move cursor to the previous row', shortcutKeyDescription: 'Arrow Up', callback: () => actionHandler('moveFocusToPreviousRowItem'), - } + }, ] } @@ -216,68 +216,3 @@ export function highlightBarKeyboardCommands( // }, ] } - -type ArticleKeyboardAction = - | 'openOriginalArticle' - | 'incrementFontSize' - | 'decrementFontSize' - | 'incrementMarginWidth' - | 'decrementMarginWidth' - | 'editDisplaySettings' - | 'setLabels' - -export function articleKeyboardCommands( - router: NextRouter | undefined, - actionHandler: (action: ArticleKeyboardAction) => void -): KeyboardCommand[] { - return [ - // { - // shortcutKeys: ['o'], - // actionDescription: 'Open original article page', - // shortcutKeyDescription: 'o', - // callback: () => actionHandler('openOriginalArticle'), - // }, - // { - // shortcutKeys: ['u'], - // actionDescription: 'Back to library', - // shortcutKeyDescription: 'u', - // callback: () => router?.push('/home'), - // }, - // { - // shortcutKeys: ['+'], - // actionDescription: 'Increase font size', - // shortcutKeyDescription: '+', - // callback: () => actionHandler('incrementFontSize'), - // }, - // { - // shortcutKeys: ['-'], - // actionDescription: 'Decrease font size', - // shortcutKeyDescription: '-', - // callback: () => actionHandler('decrementFontSize'), - // }, - // { - // shortcutKeys: [']'], - // actionDescription: 'Increase margin width', - // shortcutKeyDescription: ']', - // callback: () => actionHandler('incrementMarginWidth'), - // }, - // { - // shortcutKeys: ['['], - // actionDescription: 'Decrease margin width', - // shortcutKeyDescription: '[', - // callback: () => actionHandler('decrementMarginWidth'), - // }, - { - shortcutKeys: ['d'], - actionDescription: 'Edit Display Settings', - shortcutKeyDescription: 'd', - callback: () => actionHandler('editDisplaySettings'), - }, - // { - // shortcutKeys: ['l'], - // actionDescription: 'Edit labels', - // shortcutKeyDescription: 'l', - // callback: () => actionHandler('setLabels'), - // }, - ] -} diff --git a/packages/web/lib/keyboardShortcuts/useKeyboardShortcuts.ts b/packages/web/lib/keyboardShortcuts/useKeyboardShortcuts.ts index 1834b27c3..35af88f33 100644 --- a/packages/web/lib/keyboardShortcuts/useKeyboardShortcuts.ts +++ b/packages/web/lib/keyboardShortcuts/useKeyboardShortcuts.ts @@ -54,8 +54,8 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => { }) }) }) - - KBAR_KEYS.map((key) => currentKeys[key.toLowerCase()] = false) + + KBAR_KEYS.map((key) => (currentKeys[key.toLowerCase()] = false)) return currentKeys }, [commands]) @@ -126,7 +126,7 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => { keydownEvent.preventDefault() } }, - [applyCommands, keys] + [applyCommands, keys, metaPressed] ) const keyupListener = useCallback( diff --git a/packages/web/lib/networking/fragments/highlightFragment.ts b/packages/web/lib/networking/fragments/highlightFragment.ts index 47a938a6c..94fdfd341 100644 --- a/packages/web/lib/networking/fragments/highlightFragment.ts +++ b/packages/web/lib/networking/fragments/highlightFragment.ts @@ -1,5 +1,5 @@ import { gql } from 'graphql-request' -import { Label, labelFragment } from './labelFragment' +import { Label } from './labelFragment' export const highlightFragment = gql` fragment HighlightFields on Highlight { diff --git a/packages/web/lib/networking/fragments/labelFragment.ts b/packages/web/lib/networking/fragments/labelFragment.ts index e15b0dce2..1cf2f88cd 100644 --- a/packages/web/lib/networking/fragments/labelFragment.ts +++ b/packages/web/lib/networking/fragments/labelFragment.ts @@ -7,7 +7,7 @@ export type LabelColor = | '#7BE4FF' | '#CE88EF' | '#EF8C43' - | 'custom color' + | '#000000' export const labelFragment = gql` fragment LabelFields on Label { diff --git a/packages/web/lib/networking/mutations/updatePageMutation.ts b/packages/web/lib/networking/mutations/updatePageMutation.ts index 2520fe61e..ee15559c2 100644 --- a/packages/web/lib/networking/mutations/updatePageMutation.ts +++ b/packages/web/lib/networking/mutations/updatePageMutation.ts @@ -6,6 +6,8 @@ export type UpdatePageInput = { title: string byline?: string | undefined description: string + savedAt?: string + publishedAt?: string } export async function updatePageMutation( @@ -23,6 +25,7 @@ export async function updatePageMutation( author image description + savedAt publishedAt } } diff --git a/packages/web/lib/networking/mutations/uploadFileMutation.ts b/packages/web/lib/networking/mutations/uploadFileMutation.ts index 17625fc9c..85ce6f020 100644 --- a/packages/web/lib/networking/mutations/uploadFileMutation.ts +++ b/packages/web/lib/networking/mutations/uploadFileMutation.ts @@ -1,20 +1,13 @@ import { gqlFetcher } from '../networkHelpers' import { v4 as uuidv4 } from 'uuid' - type UploadFileInput = { url: string - contentType: string + contentType: string createPageEntry?: boolean clientRequestId?: string } -type UploadFileOutput = { - jobId?: string - url?: string - clientRequestId?: string -} - type UploadFileResponseData = { uploadFileRequest?: UploadFileData errorCodes?: unknown[] diff --git a/packages/web/lib/networking/mutations/uploadImportFileMutation.ts b/packages/web/lib/networking/mutations/uploadImportFileMutation.ts index 46ac248d9..055fe9078 100644 --- a/packages/web/lib/networking/mutations/uploadImportFileMutation.ts +++ b/packages/web/lib/networking/mutations/uploadImportFileMutation.ts @@ -1,5 +1,4 @@ import { gqlFetcher } from '../networkHelpers' -import { v4 as uuidv4 } from 'uuid' export enum UploadImportFileType { URL_LIST = 'URL_LIST', diff --git a/packages/web/lib/networking/queries/useGetApiKeysQuery.tsx b/packages/web/lib/networking/queries/useGetApiKeysQuery.tsx index c1b6b0fa8..24a588fb8 100644 --- a/packages/web/lib/networking/queries/useGetApiKeysQuery.tsx +++ b/packages/web/lib/networking/queries/useGetApiKeysQuery.tsx @@ -48,7 +48,7 @@ export function useGetApiKeysQuery(): ApiKeysQueryResponse { } ` - const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) + const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) try { if (data) { diff --git a/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx b/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx index bfb4c4e14..c12443eec 100644 --- a/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx @@ -1,8 +1,6 @@ import { gql } from 'graphql-request' -import useSWRImmutable, { Cache } from 'swr' +import useSWRImmutable from 'swr' import { makeGqlFetcher, RequestContext, ssrFetcher } from '../networkHelpers' -import { ScopedMutator } from 'swr/dist/types' -import { LibraryItems } from './useGetLibraryItemsQuery' type ArticleQueryInput = { username?: string @@ -25,10 +23,7 @@ export type ArticleAttributes = { } const query = gql` - query GetArticle( - $username: String! - $slug: String! - ) { + query GetArticle($username: String!, $slug: String!) { article(username: $username, slug: $slug) { ... on ArticleSuccess { article { @@ -58,7 +53,7 @@ export function useGetArticleOriginalHtmlQuery({ ) const resultData: ArticleData | undefined = data as ArticleData - console.log("RESULT", JSON.stringify(data)) + console.log('RESULT', JSON.stringify(data)) return resultData?.article.article.originalHtml } @@ -67,7 +62,12 @@ export async function originalHtmlQuery( context: RequestContext, input: ArticleQueryInput ): Promise { - const resultData = (await ssrFetcher(context, query, input, false)) as ArticleData + const resultData = (await ssrFetcher( + context, + query, + input, + false + )) as ArticleData console.log(JSON.stringify(resultData)) // if (resultData?.article.article.originalHtml) { // return resultData?.article.article.originalHtml diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index f6a361865..6f76b05c6 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -105,7 +105,7 @@ export function useGetArticleQuery({ includeFriendsHighlights, } - const { data, error, mutate } = useSWRImmutable( + const { data, error } = useSWRImmutable( slug ? [query, username, slug, includeFriendsHighlights] : null, makeGqlFetcher(variables) ) @@ -159,7 +159,7 @@ export const removeItemFromCache = ( try { const mappedCache = cache as Map mappedCache.forEach((value: any, key) => { - if (typeof value == 'object' && 'search' in value) { + if (value && typeof value == 'object' && 'search' in value) { const search = value.search as LibraryItems const idx = search.edges.findIndex((edge) => edge.node.id == itemId) if (idx > -1) { diff --git a/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx b/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx index 6c0aafbae..e61ef35ae 100644 --- a/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx @@ -11,8 +11,7 @@ export interface Integration { updatedAt: Date } -export type IntegrationType = - | 'READWISE' +export type IntegrationType = 'READWISE' interface IntegrationsQueryResponse { isValidating: boolean @@ -49,7 +48,7 @@ export function useGetIntegrationsQuery(): IntegrationsQueryResponse { } ` - const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) + const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) console.log('integrations data', data) try { diff --git a/packages/web/lib/networking/queries/useGetLabelsQuery.tsx b/packages/web/lib/networking/queries/useGetLabelsQuery.tsx index 211c53178..a4af02c2c 100644 --- a/packages/web/lib/networking/queries/useGetLabelsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLabelsQuery.tsx @@ -34,7 +34,7 @@ export function useGetLabelsQuery(): LabelsQueryResponse { ${labelFragment} ` - const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) + const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) try { if (data) { diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 57d1635e9..0d4d7d686 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -9,6 +9,7 @@ import { unsubscribeMutation } from '../mutations/unsubscribeMutation' import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation' import { Label } from './../fragments/labelFragment' import { showErrorToast, showSuccessToast } from '../../toastHelpers' +import { Highlight, highlightFragment } from '../fragments/highlightFragment' export type LibraryItemsQueryInput = { limit: number @@ -82,7 +83,10 @@ export type LibraryItemNode = { siteName?: string subscription?: string readAt?: string + savedAt?: string + wordsCount?: number recommendations?: Recommendation[] + highlights?: Highlight[] } export type Recommendation = { @@ -166,6 +170,8 @@ export function useGetLibraryItemsQuery({ siteName subscription readAt + savedAt + wordsCount recommendations { id name @@ -178,6 +184,9 @@ export function useGetLibraryItemsQuery({ } recommendedAt } + highlights { + ...HighlightFields + } } } pageInfo { @@ -193,6 +202,7 @@ export function useGetLibraryItemsQuery({ } } } + ${highlightFragment} ` const variables = { diff --git a/packages/web/lib/networking/queries/useGetNewsletterEmailsQuery.tsx b/packages/web/lib/networking/queries/useGetNewsletterEmailsQuery.tsx index a8c57cdd4..f15415193 100644 --- a/packages/web/lib/networking/queries/useGetNewsletterEmailsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetNewsletterEmailsQuery.tsx @@ -45,7 +45,7 @@ export function useGetNewsletterEmailsQuery(): NewsletterEmailsQueryResponse { } ` - const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) + const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) try { if (data) { diff --git a/packages/web/lib/networking/queries/useGetRulesQuery.tsx b/packages/web/lib/networking/queries/useGetRulesQuery.tsx index cb711fea1..c193196b0 100644 --- a/packages/web/lib/networking/queries/useGetRulesQuery.tsx +++ b/packages/web/lib/networking/queries/useGetRulesQuery.tsx @@ -63,24 +63,13 @@ export function useGetRulesQuery(): RulesQueryResponse { } ` - const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) + const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) try { if (data) { const result = data as RulesQueryResponseData const rules = result.rules.rules as Rule[] - const actions: RuleAction[] = [ - { - type: RuleActionType.SendNotification, - params: [] as string[], - }, - { - type: RuleActionType.AddLabel, - params: ['2dfd9ce2-cc9f-11ec-b535-3be2782c2107'], - }, - ] - return { isValidating, rules: rules ?? [], diff --git a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx index bacb0e5bb..051b20d70 100644 --- a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx @@ -56,7 +56,7 @@ export function useGetSubscriptionsQuery(): SubscriptionsQueryResponse { } ` - const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) + const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) try { if (data) { diff --git a/packages/web/lib/networking/queries/useGetWebhooksQuery.tsx b/packages/web/lib/networking/queries/useGetWebhooksQuery.tsx index dedd16ceb..0ac5c802f 100644 --- a/packages/web/lib/networking/queries/useGetWebhooksQuery.tsx +++ b/packages/web/lib/networking/queries/useGetWebhooksQuery.tsx @@ -58,7 +58,7 @@ export function useGetWebhooksQuery(): WebhooksQueryResponse { } ` - const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) + const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) console.log('webhooks data', data) try { diff --git a/packages/web/lib/themeUpdater.tsx b/packages/web/lib/themeUpdater.tsx index 76e454946..24668b03b 100644 --- a/packages/web/lib/themeUpdater.tsx +++ b/packages/web/lib/themeUpdater.tsx @@ -3,8 +3,6 @@ import { lighterTheme, darkTheme, darkerTheme, - sepiaTheme, - charcoalTheme, } from '../components/tokens/stitches.config' import { userPersonalizationMutation } from './networking/mutations/userPersonalizationMutation' @@ -57,7 +55,7 @@ export function currentThemeName(): string { } } -function currentTheme(): ThemeId | undefined { +export function currentTheme(): ThemeId | undefined { if (typeof window === 'undefined') { return undefined } diff --git a/packages/web/next.config.js b/packages/web/next.config.js index 16ae40aa3..8d3c2378a 100644 --- a/packages/web/next.config.js +++ b/packages/web/next.config.js @@ -1,4 +1,3 @@ - const moduleExports = { images: { domains: [ @@ -148,4 +147,7 @@ const moduleExports = { }, } -module.exports = moduleExports +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}) +module.exports = withBundleAnalyzer(moduleExports) diff --git a/packages/web/package.json b/packages/web/package.json index 4b821c258..aca49392a 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -26,6 +26,8 @@ "@radix-ui/react-popover": "^0.1.1", "@radix-ui/react-progress": "^1.0.1", "@radix-ui/react-separator": "^0.1.0", + "@radix-ui/react-slider": "^1.1.0", + "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tooltip": "^0.1.7", "@segment/analytics-next": "^1.33.5", "@sentry/nextjs": "^6.16.1", @@ -34,6 +36,7 @@ "axios": "^1.2.0", "color2k": "^2.0.0", "cookie": "^0.5.0", + "dayjs": "^1.11.7", "diff-match-patch": "^1.0.5", "downshift": "^6.1.9", "graphql-request": "^3.6.1", @@ -43,7 +46,6 @@ "phosphor-react": "^1.4.0", "pspdfkit": "^2022.2.3", "react": "^17.0.2", - "react-apple-login": "^1.1.3", "react-colorful": "^5.5.1", "react-dom": "^17.0.2", "react-dropzone": "^14.2.3", @@ -59,6 +61,7 @@ }, "devDependencies": { "@babel/core": "^7.17.5", + "@next/bundle-analyzer": "^13.2.1", "@storybook/addon-actions": "^6.4.22", "@storybook/addon-essentials": "^6.4.22", "@storybook/addon-interactions": "^6.4.22", diff --git a/packages/web/pages/404.tsx b/packages/web/pages/404.tsx index e2cc748ce..80e44ab9d 100644 --- a/packages/web/pages/404.tsx +++ b/packages/web/pages/404.tsx @@ -1,5 +1,4 @@ import Head from 'next/head' -import { useRouter } from 'next/router' import { ErrorLayout } from '../components/templates/ErrorLayout' import { SettingsLayout } from '../components/templates/SettingsLayout' @@ -10,7 +9,7 @@ export default function Custom404(): JSX.Element { Page Not Found - + ) diff --git a/packages/web/pages/[username]/[slug]/highlights/[highlightId].tsx b/packages/web/pages/[username]/[slug]/highlights/[highlightId].tsx deleted file mode 100644 index 897b5389b..000000000 --- a/packages/web/pages/[username]/[slug]/highlights/[highlightId].tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { GetServerSideProps } from 'next' -import { captureException, flush } from '@sentry/nextjs' -import { ArticleHighlights } from '../../../../components/templates/ArticleHighlights' -import { highlightsBaseURL, webBaseURL } from '../../../../lib/appConfig' -import objectToHash from '../../../../lib/highlights/objectToHash' -import { - PublicArticleAttributes, - publicArticleQuery, -} from '../../../../lib/networking/queries/useGetPublicArticleQuery' -import { useEffect } from 'react' - -type PublicHighlightPageProps = { - publicArticle: PublicArticleAttributes - showAllHighlights: boolean - selectedHighlightId?: string - previewImagePath?: string -} - -export default function PublicHighlightPage( - props: PublicHighlightPageProps -): JSX.Element { - useEffect(() => { - window.analytics?.track('public_highlight_read', { - link: props.publicArticle.id, - slug: props.publicArticle.slug, - url: props.publicArticle.url, - }) - }, [props.publicArticle.url]) - - return ( - <> - - - ) -} - -export const getServerSideProps: GetServerSideProps< - PublicHighlightPageProps -> = async (ctx) => { - const slug = ctx.query.slug as string - const username = ctx.query.username as string - const selectedHighlightId = ctx.query.highlightId as string - - try { - const publicArticle = await publicArticleQuery(ctx, { username, slug }) - - if (publicArticle) { - const selectedHighlight = await publicArticle.highlights.find( - (h) => h.shortId === selectedHighlightId - ) - - if (selectedHighlight) { - const previewImageMeta = { - highlightsCount: publicArticle.highlights.length, - annotationsCount: publicArticle.highlights.filter( - (h) => !!h.annotation - ).length, - quote: selectedHighlight.quote, - prefix: selectedHighlight.prefix, - suffix: selectedHighlight.suffix, - annotation: selectedHighlight.annotation, - } - const previewImageHash = objectToHash(previewImageMeta) - const previewServiceUrl = `${webBaseURL}/${username}/${slug}/highlights/${selectedHighlightId}/preview?pih=${previewImageHash}` - const previewImagePath = `${highlightsBaseURL}/preview?url=${encodeURIComponent( - previewServiceUrl - )}` - - return { - props: { - username, - publicArticle, - selectedHighlightId, - previewImagePath, - showAllHighlights: false, - }, - } - } else { - throw new Error( - 'public article highlights query failed - no highlights' - ) - } - } else { - throw new Error('public article highlights query failed - no article') - } - } catch (error) { - captureException(error) - // Flushing before returning is necessary if deploying to Vercel, see - // https://vercel.com/docs/platform/limits#streaming-responses - await flush(2000) - return { notFound: true } - } -} diff --git a/packages/web/pages/[username]/[slug]/highlights/[highlightId]/preview.tsx b/packages/web/pages/[username]/[slug]/highlights/[highlightId]/preview.tsx deleted file mode 100644 index 40ab40f59..000000000 --- a/packages/web/pages/[username]/[slug]/highlights/[highlightId]/preview.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { GetServerSideProps } from 'next' -import Head from 'next/head' -import { useRouter } from 'next/router' -import { useEffect, useMemo } from 'react' -import { - Box, - SpanBox, -} from '../../../../../components/elements/LayoutPrimitives' -import { - HighlightFooter, - PublicHighlightView, -} from '../../../../../components/patterns/HighlightView' -import objectToHash from '../../../../../lib/highlights/objectToHash' -import { - PublicArticleAttributes, - publicArticleQuery, -} from '../../../../../lib/networking/queries/useGetPublicArticleQuery' -import { captureException, flush } from '@sentry/nextjs' - -type PublicHighlightPageProps = { - publicArticle: PublicArticleAttributes - showAllHighlights: boolean - selectedHighlightId?: string - previewImageFileName?: string -} - -export default function PublicHighlightPage( - props: PublicHighlightPageProps -): JSX.Element { - const router = useRouter() - const selectedHighlight = useMemo(() => { - if (!props.selectedHighlightId) { - return null - } - return props.publicArticle.highlights.find( - (h) => h.shortId === props.selectedHighlightId - ) - }, [props.selectedHighlightId, props.publicArticle]) - - const articleSite = useMemo(() => { - try { - const url = new URL(props.publicArticle.url) - return url.hostname - } catch (e) { - console.log('error ', e) - return '' - } - }, [props.publicArticle.url]) - - // Adjusting the aspect ratio accordingly to the query parameter - useEffect(() => { - if (router.isReady && router.query.adjustAspectRatio) { - const highlightContainer = document.getElementById( - 'selected_highlight_wrapper' - ) - const footer = document.getElementById('selected_highlight_footer') - if (!highlightContainer || !footer) return - - const getTextY = (): number => footer.getBoundingClientRect().bottom - const getContainerY = (): number => - highlightContainer.getBoundingClientRect().bottom - - let widthPercent = 100 - - // We are gradually decreasing the width of the container until the text bottom spacing is reduced in the container - while (getContainerY() - getTextY() > 25) { - widthPercent = widthPercent - 2 - highlightContainer.style.width = widthPercent + '%' - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return ( - <> - - {props.previewImageFileName && ( - - )} - {props.previewImageFileName && ( - - )} - - {selectedHighlight && ( - - - - - - - - )} - - ) -} - -export const getServerSideProps: GetServerSideProps< - PublicHighlightPageProps -> = async (ctx) => { - const slug = ctx.query.slug as string - const username = ctx.query.username as string - const selectedHighlightId = ctx.query.highlightId as string - - try { - const publicArticle = await publicArticleQuery(ctx, { username, slug }) - - if (publicArticle) { - const selectedHighlight = await publicArticle.highlights.find( - (h) => h.shortId === selectedHighlightId - ) - if (!selectedHighlight) { - return { notFound: true } - } - - const previewImageMeta = { - highlightsCount: publicArticle.highlights.length, - annotationsCount: publicArticle.highlights.filter((h) => !!h.annotation) - .length, - quote: selectedHighlight?.quote, - prefix: selectedHighlight?.prefix, - suffix: selectedHighlight?.suffix, - annotation: selectedHighlight?.annotation, - } - const previewImageHash = objectToHash(previewImageMeta) - const previewImageFileName = `${username}/${slug}/highlights/${selectedHighlightId}/preview_${previewImageHash}.png` - - return { - props: { - username, - publicArticle, - selectedHighlightId, - previewImageFileName, - showAllHighlights: false, - }, - } - } else { - throw new Error('public article query failed') - } - } catch (error) { - captureException(error) - // Flushing before returning is necessary if deploying to Vercel, see - // https://vercel.com/docs/platform/limits#streaming-responses - await flush(2000) - return { notFound: true } - } -} diff --git a/packages/web/pages/[username]/[slug]/highlights/index.tsx b/packages/web/pages/[username]/[slug]/highlights/index.tsx deleted file mode 100644 index 4f15d2203..000000000 --- a/packages/web/pages/[username]/[slug]/highlights/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { GetServerSideProps } from 'next' -import { ArticleHighlights } from '../../../../components/templates/ArticleHighlights' -import { - PublicArticleAttributes, - publicArticleQuery, -} from '../../../../lib/networking/queries/useGetPublicArticleQuery' -import { captureException, flush } from '@sentry/nextjs' -import { useEffect } from 'react' - -type PublicHighlightsPageProps = { - publicArticle: PublicArticleAttributes -} - -export default function PublicHighlightsPage( - props: PublicHighlightsPageProps -): JSX.Element { - - useEffect(() => { - window.analytics?.track('public_link_read', { - link: props.publicArticle.id, - slug: props.publicArticle.slug, - url: props.publicArticle.url - }) - }, [props.publicArticle.url]) - - return ( - - ) -} - -export const getServerSideProps: GetServerSideProps< - PublicHighlightsPageProps -> = async (ctx) => { - const slug = ctx.query.slug as string - const username = ctx.query.username as string - - try { - const publicArticle = await publicArticleQuery(ctx, { username, slug }) - - if (publicArticle) { - if (publicArticle.highlights.length === 0) { - return { - redirect: { - destination: publicArticle.url, - permanent: false, - }, - } - } - - return { - props: { - username, - publicArticle, - }, - } - } else { - throw new Error('article highlights request failed') - } - } catch (error) { - captureException(error) - // Flushing before returning is necessary if deploying to Vercel, see - // https://vercel.com/docs/platform/limits#streaming-responses - await flush(2000) - return { notFound: true } - } -} diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index eb9647df4..af9c9b364 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -9,12 +9,9 @@ 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts' -import { - articleKeyboardCommands, - navigationCommands, -} from '../../../lib/keyboardShortcuts/navigationShortcuts' +import { navigationCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts' import dynamic from 'next/dynamic' import { webBaseURL } from '../../../lib/appConfig' import { Toaster } from 'react-hot-toast' @@ -24,7 +21,6 @@ import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeH import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation' import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation' import Script from 'next/script' -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' @@ -38,6 +34,10 @@ import { useRegisterActions } from 'kbar' import { deleteLinkMutation } from '../../../lib/networking/mutations/deleteLinkMutation' import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal' import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' +import { ReaderHeader } from '../../../components/templates/reader/ReaderHeader' +import { EditArticleModal } from '../../../components/templates/homeFeed/EditItemModals' +import { VerticalArticleActionsMenu } from '../../../components/templates/article/VerticalArticleActions' +import { HeaderSpacer } from '../../../components/templates/homeFeed/HeaderSpacer' const PdfArticleContainerNoSSR = dynamic( () => import('./../../../components/templates/article/PdfArticleContainer'), @@ -49,6 +49,8 @@ export default function Home(): JSX.Element { const { cache, mutate } = useSWRConfig() const scrollRef = useRef(null) const { slug } = router.query + + const [showEditModal, setShowEditModal] = useState(false) const [showHighlightsModal, setShowHighlightsModal] = useState(false) const { viewerData } = useGetViewerQuery() const readerSettings = useReaderSettings() @@ -132,6 +134,9 @@ export default function Home(): JSX.Element { case 'showHighlights': setShowHighlightsModal(true) break + case 'showEditModal': + setShowEditModal(true) + break default: readerSettings.actionHandler(action, arg) break @@ -161,12 +166,6 @@ export default function Home(): JSX.Element { } }, [actionHandler]) - useKeyboardShortcuts( - articleKeyboardCommands(router, async (action) => { - actionHandler(action) - }) - ) - useEffect(() => { if (article && viewerData?.me) { window.analytics?.track('link_read', { @@ -191,7 +190,7 @@ export default function Home(): JSX.Element { }) router.push(`/home`) } - }, [article]) + }, [article, cache, mutate, router]) useRegisterActions( [ @@ -209,7 +208,15 @@ export default function Home(): JSX.Element { section: 'Article', name: 'Return to library', shortcut: ['u'], - perform: () => router.push(`/home`), + perform: () => { + const query = window.sessionStorage.getItem('q') + if (query) { + router.push(`/home?${query}`) + return + } else { + router.push(`/home`) + } + }, }, { id: 'archive', @@ -256,6 +263,13 @@ export default function Home(): JSX.Element { setShowHighlightsModal(true) }, }, + { + id: 'edit_title', + section: 'Article', + name: 'Edit title and description', + shortcut: ['i'], + perform: () => setShowEditModal(true), + }, ], [] ) @@ -291,13 +305,28 @@ export default function Home(): JSX.Element { /> + + + + + {article?.contentReader == 'PDF' && } + {article && viewerData?.me ? ( @@ -347,6 +376,8 @@ export default function Home(): JSX.Element { labels={labels} showHighlightsModal={showHighlightsModal} setShowHighlightsModal={setShowHighlightsModal} + justifyText={readerSettings.justifyText ?? undefined} + highContrastText={readerSettings.highContrastText ?? undefined} articleMutations={{ createHighlightMutation, deleteHighlightMutation, @@ -383,10 +414,10 @@ export default function Home(): JSX.Element { {readerSettings.showEditDisplaySettingsModal && ( + readerSettings={readerSettings} + onOpenChange={() => { readerSettings.setShowEditDisplaySettingsModal(false) - } + }} /> )} {readerSettings.showDeleteConfirmation && ( @@ -396,6 +427,19 @@ export default function Home(): JSX.Element { onOpenChange={() => readerSettings.setShowDeleteConfirmation(false)} /> )} + {article && showEditModal && ( + setShowEditModal(false)} + updateArticle={(title, author, description, savedAt, publishedAt) => { + article.title = title + article.author = author + article.description = description + article.savedAt = savedAt + article.publishedAt = publishedAt + }} + /> + )} ) } diff --git a/packages/web/pages/[username]/links/[id].tsx b/packages/web/pages/[username]/links/[id].tsx index 90ef594c8..3b7ca4109 100644 --- a/packages/web/pages/[username]/links/[id].tsx +++ b/packages/web/pages/[username]/links/[id].tsx @@ -32,7 +32,7 @@ export default function ArticleSavingRequestPage(): JSX.Element { headerToolbarControl={ @@ -44,42 +44,45 @@ export default function ArticleSavingRequestPage(): JSX.Element { }} > - - + - - {articleId ? : } - + {articleId ? : } + ) @@ -121,7 +124,5 @@ function PrimaryContent(props: PrimaryContentProps): JSX.Element { router.replace(successRedirectPath) } - return ( - - ) + return } diff --git a/packages/web/pages/_app.tsx b/packages/web/pages/_app.tsx index 7daafb7dc..ad224caca 100644 --- a/packages/web/pages/_app.tsx +++ b/packages/web/pages/_app.tsx @@ -19,13 +19,17 @@ import { KBarSearch, Priority, } from 'kbar' -import { animatorStyle, KBarResultsComponents, searchStyle } from '../components/elements/KBar' +import { + animatorStyle, + KBarResultsComponents, + searchStyle, +} from '../components/elements/KBar' import { darkenTheme, lightenTheme } from '../lib/themeUpdater' TopBarProgress.config({ barColors: { - "0": '#FFD234', - "1.0": '#FFD234', + '0': '#FFD234', + '1.0': '#FFD234', }, shadowBlur: 0, barThickness: 2, @@ -99,7 +103,7 @@ function OmnivoreApp({ Component, pageProps }: AppProps): JSX.Element { return ( - + diff --git a/packages/web/pages/_document.tsx b/packages/web/pages/_document.tsx index 927d240dc..e5c96fe8b 100644 --- a/packages/web/pages/_document.tsx +++ b/packages/web/pages/_document.tsx @@ -53,25 +53,61 @@ export default class Document extends NextDocument {