diff --git a/packages/web/components/nav-containers/highlights.tsx b/packages/web/components/nav-containers/highlights.tsx deleted file mode 100644 index ef1358249..000000000 --- a/packages/web/components/nav-containers/highlights.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { NavigationLayout } from '../templates/NavigationLayout' -import { Box, HStack, VStack } from '../elements/LayoutPrimitives' -import { useFetchMore } from '../../lib/hooks/useFetchMoreScroll' -import { useCallback, useMemo, useState } from 'react' -import { useGetHighlights } from '../../lib/networking/queries/useGetHighlights' -import { Highlight } from '../../lib/networking/fragments/highlightFragment' -import { NextRouter, useRouter } from 'next/router' -import { - UserBasicData, - useGetViewerQuery, -} from '../../lib/networking/queries/useGetViewerQuery' -import { SetHighlightLabelsModalPresenter } from '../templates/article/SetLabelsModalPresenter' -import { TrashIcon } from '../elements/icons/TrashIcon' -import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' -import { ConfirmationModal } from '../patterns/ConfirmationModal' -import { deleteHighlightMutation } from '../../lib/networking/mutations/deleteHighlightMutation' -import { LabelChip } from '../elements/LabelChip' -import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' -import { timeAgo } from '../patterns/LibraryCards/LibraryCardStyles' -import { HighlightHoverActions } from '../patterns/HighlightHoverActions' -import { - autoUpdate, - offset, - size, - useFloating, - useHover, - useInteractions, -} from '@floating-ui/react' -import { highlightColor } from '../../lib/themeUpdater' - -import { HighlightViewNote } from '../patterns/HighlightNotes' -import { theme } from '../tokens/stitches.config' - -const PAGE_SIZE = 10 - -export function HighlightsContainer(): JSX.Element { - const router = useRouter() - const viewer = useGetViewerQuery() - const [showFilterMenu, setShowFilterMenu] = useState(false) - const [_, setShowAddLinkModal] = useState(false) - - const { isLoading, setSize, size, data, mutate } = useGetHighlights({ - first: PAGE_SIZE, - }) - - const hasMore = useMemo(() => { - if (!data) { - return false - } - return data[data.length - 1].highlights.pageInfo.hasNextPage - }, [data]) - - const handleFetchMore = useCallback(() => { - if (isLoading || !hasMore) { - return - } - setSize(size + 1) - }, [isLoading, hasMore, setSize, size]) - - useFetchMore(handleFetchMore) - - const highlights = useMemo(() => { - if (!data) { - return [] - } - return data.flatMap((res) => res.highlights.edges.map((edge) => edge.node)) - }, [data]) - - return ( - - {highlights.map((highlight) => { - return ( - viewer.viewerData?.me && ( - - ) - ) - })} - - ) -} - -type HighlightCardProps = { - highlight: Highlight - viewer: UserBasicData - router: NextRouter - mutate: () => void -} - -type HighlightAnnotationProps = { - highlight: Highlight -} - -function HighlightAnnotation({ - highlight, -}: HighlightAnnotationProps): JSX.Element { - const [noteMode, setNoteMode] = useState<'edit' | 'preview'>('preview') - const [annotation, setAnnotation] = useState(highlight.annotation) - - return ( - { - setAnnotation(highlight.annotation) - }} - /> - ) -} - -function HighlightCard(props: HighlightCardProps): JSX.Element { - const [isOpen, setIsOpen] = useState(false) - const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = - useState(undefined) - const [labelsTarget, setLabelsTarget] = useState( - undefined - ) - - const viewInReader = useCallback( - (highlightId: string) => { - const router = props.router - const viewer = props.viewer - const item = props.highlight.libraryItem - - if (!router || !router.isReady || !viewer || !item) { - showErrorToast('Error navigating to highlight') - return - } - - router.push( - { - pathname: '/[username]/[slug]', - query: { - username: viewer.profile.username, - slug: item.slug, - }, - hash: highlightId, - }, - `${viewer.profile.username}/${item.slug}#${highlightId}`, - { - scroll: false, - } - ) - }, - [props.highlight.libraryItem, props.viewer, props.router] - ) - - const { refs, floatingStyles, context } = useFloating({ - open: isOpen, - onOpenChange: setIsOpen, - middleware: [ - offset({ - mainAxis: -25, - }), - size(), - ], - placement: 'top-end', - whileElementsMounted: autoUpdate, - }) - - const hover = useHover(context) - - const { getReferenceProps, getFloatingProps } = useInteractions([hover]) - - return ( - - - - - - - {timeAgo(props.highlight.updatedAt)} - - {props.highlight.quote && ( - - {props.highlight.quote} - - )} - - {props.highlight.labels && ( - - {props.highlight.labels.map((label) => { - return ( - - ) - })} - - )} - - {props.highlight.libraryItem?.title} - - - {props.highlight.libraryItem?.author} - - {showConfirmDeleteHighlightId && ( - { - ;(async () => { - const highlightId = showConfirmDeleteHighlightId - const success = await deleteHighlightMutation( - props.highlight.libraryItem?.id || '', - showConfirmDeleteHighlightId - ) - props.mutate() - if (success) { - showSuccessToast('Highlight deleted.', { - position: 'bottom-right', - }) - const event = new CustomEvent('deleteHighlightbyId', { - detail: highlightId, - }) - document.dispatchEvent(event) - } else { - showErrorToast('Error deleting highlight', { - position: 'bottom-right', - }) - } - })() - setShowConfirmDeleteHighlightId(undefined) - }} - onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)} - icon={ - - } - /> - )} - {labelsTarget && ( - { - // Don't actually need to do something here - console.log('update highlight: ', highlight) - }} - onOpenChange={() => { - props.mutate() - setLabelsTarget(undefined) - }} - /> - )} - - ) -} diff --git a/packages/web/components/nav-containers/home.tsx b/packages/web/components/nav-containers/home.tsx deleted file mode 100644 index 8c4a3d6b6..000000000 --- a/packages/web/components/nav-containers/home.tsx +++ /dev/null @@ -1,887 +0,0 @@ -import * as HoverCard from '@radix-ui/react-hover-card' -import { styled } from '@stitches/react' -import { useRouter } from 'next/router' -import { useCallback, useEffect, useMemo, useReducer, useState } from 'react' -import { Button } from '../elements/Button' -import { AddToLibraryActionIcon } from '../elements/icons/home/AddToLibraryActionIcon' -import { ArchiveActionIcon } from '../elements/icons/home/ArchiveActionIcon' -import { CommentActionIcon } from '../elements/icons/home/CommentActionIcon' -import { RemoveActionIcon } from '../elements/icons/home/RemoveActionIcon' -import { ShareActionIcon } from '../elements/icons/home/ShareActionIcon' -import Pagination from '../elements/Pagination' -import { timeAgo } from '../patterns/LibraryCards/LibraryCardStyles' -import { theme } from '../tokens/stitches.config' -import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme' -import { useGetHiddenHomeSection } from '../../lib/networking/queries/useGetHiddenHomeSection' -import { - HomeItem, - HomeItemSource, - HomeItemSourceType, - HomeSection, - useGetHomeItems, -} from '../../lib/networking/queries/useGetHome' -import { - SubscriptionType, - useGetSubscriptionsQuery, -} from '../../lib/networking/queries/useGetSubscriptionsQuery' -import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' -import { Toaster } from 'react-hot-toast' -import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery' - -export function HomeContainer(): JSX.Element { - const router = useRouter() - const homeData = useGetHomeItems() - const { viewerData } = useGetViewerQuery() - - useApplyLocalTheme() - - const viewerUsername = useMemo(() => { - return viewerData?.me?.profile.username - }, [viewerData]) - - useEffect(() => { - window.localStorage.setItem('nav-return', router.asPath) - }, [router.asPath]) - - return ( - - - - {homeData.sections?.map((homeSection, idx) => { - if (homeSection.items.length < 1) { - console.log('empty home section: ', homeSection) - return - } - switch (homeSection.layout) { - case 'just_added': - return ( - - ) - case 'top_picks': - return ( - - ) - case 'quick_links': - return ( - - ) - case 'hidden': - return ( - - ) - default: - console.log('unknown home section: ', homeSection) - return - } - })} - - - ) -} - -type HomeSectionProps = { - homeSection: HomeSection - viewerUsername: string | undefined -} - -const JustAddedHomeSection = (props: HomeSectionProps): JSX.Element => { - const router = useRouter() - return ( - - - - {props.homeSection.title} - - - - - - - {props.homeSection.items.map((homeItem) => { - return - })} - - - ) -} - -const TopPicksHomeSection = (props: HomeSectionProps): JSX.Element => { - const listReducer = ( - state: HomeItem[], - action: { - type: string - itemId?: string - items?: HomeItem[] - } - ) => { - console.log('handling action: ', action) - switch (action.type) { - case 'RESET': - return action.items ?? [] - case 'REMOVE_ITEM': - return state.filter((item) => item.id !== action.itemId) - default: - throw new Error() - } - } - - const [items, dispatchList] = useReducer(listReducer, []) - - function handleDelete(item: HomeItem) { - dispatchList({ - type: 'REMOVE_ITEM', - itemId: item.id, - }) - } - - useEffect(() => { - dispatchList({ - type: 'RESET', - items: props.homeSection.items, - }) - }, [props]) - - return ( - - {items.length > 0 && ( - - {props.homeSection.title} - - )} - - ( - - )} - /> - - ) -} - -const QuickLinksHomeSection = (props: HomeSectionProps): JSX.Element => { - return ( - - - {props.homeSection.title} - - - ( - - )} - /> - - ) -} - -const HiddenHomeSection = (props: HomeSectionProps): JSX.Element => { - const [isHidden, setIsHidden] = useState(true) - return ( - - setIsHidden(!isHidden)} - > - - {props.homeSection.title} - - - {isHidden ? 'Show' : 'Hide'} - - - - {isHidden ? <> : } - - ) -} - -const HiddenHomeSectionView = (): JSX.Element => { - const hiddenSectionData = useGetHiddenHomeSection() - - if (hiddenSectionData.error) { - return Error loading hidden section - } - - if (hiddenSectionData.isValidating) { - return Loading... - } - - if (!hiddenSectionData.section) { - return No hidden section data - } - - return ( - - {hiddenSectionData.section.items.map((homeItem) => { - return - })} - - ) -} - -const CoverImage = styled('img', { - objectFit: 'cover', -}) - -type HomeItemViewProps = { - homeItem: HomeItem - viewerUsername?: string | undefined -} - -const TimeAgo = (props: HomeItemViewProps): JSX.Element => { - return ( - - {timeAgo(props.homeItem.date)} - - ) -} - -const Title = (props: HomeItemViewProps): JSX.Element => { - return ( - - {props.homeItem.title} - - ) -} - -const TitleSmall = (props: HomeItemViewProps): JSX.Element => { - return ( - - {props.homeItem.title} - - ) -} - -type PreviewContentProps = { - previewContent?: string - maxLines?: string -} - -const PreviewContent = (props: PreviewContentProps): JSX.Element => { - return ( - - {props.previewContent ?? ''} - - ) -} - -const JustAddedItemView = (props: HomeItemViewProps): JSX.Element => { - const router = useRouter() - - return ( - { - const path = `/${props.viewerUsername ?? 'me'}/${props.homeItem.slug}` - if (event.metaKey || event.ctrlKey) { - window.open(path, '_blank') - } else { - router.push(path) - } - }} - > - - - - - - - - - - ) -} - -type TopPicksItemViewProps = { - dispatchList: (args: { type: string; itemId?: string }) => void -} - -const TopPicksItemView = ( - props: HomeItemViewProps & TopPicksItemViewProps -): JSX.Element => { - const router = useRouter() - return ( - { - const path = `/${props.viewerUsername ?? 'me'}/${props.homeItem.slug}` - if (event.metaKey || event.ctrlKey) { - window.open(path, '_blank') - } else { - router.push(path) - } - }} - alignment="start" - > - - - - - - - - - {props.homeItem.thumbnail && ( - - )} - - <PreviewContent - previewContent={props.homeItem.previewContent} - maxLines="6" - /> - </Box> - <SpanBox css={{ px: '20px' }}></SpanBox> - <HStack css={{ gap: '10px', my: '15px', px: '20px' }}> - {props.homeItem.canSave && ( - <Button - style="homeAction" - onClick={(event) => { - event.preventDefault() - event.stopPropagation() - - props.dispatchList({ - type: 'REMOVE_ITEM', - itemId: props.homeItem.id, - }) - }} - > - <AddToLibraryActionIcon - color={theme.colors.homeActionIcons.toString()} - /> - </Button> - )} - {props.homeItem.canArchive && ( - <Button - style="homeAction" - onClick={(event) => { - event.preventDefault() - event.stopPropagation() - - props.dispatchList({ - type: 'REMOVE_ITEM', - itemId: props.homeItem.id, - }) - }} - > - <ArchiveActionIcon - color={theme.colors.homeActionIcons.toString()} - /> - </Button> - )} - {props.homeItem.canDelete && ( - <Button - style="homeAction" - onClick={(event) => { - event.preventDefault() - event.stopPropagation() - - props.dispatchList({ - type: 'REMOVE_ITEM', - itemId: props.homeItem.id, - }) - }} - > - <RemoveActionIcon color={theme.colors.homeActionIcons.toString()} /> - </Button> - )} - {props.homeItem.canShare && ( - <Button - style="homeAction" - onClick={(event) => { - event.preventDefault() - event.stopPropagation() - }} - > - <ShareActionIcon color={theme.colors.homeActionIcons.toString()} /> - </Button> - )} - </HStack> - <Box - css={{ mt: '15px', width: '100%', height: '1px', bg: '$homeDivider' }} - /> - </VStack> - ) -} - -const QuickLinkHomeItemView = (props: HomeItemViewProps): JSX.Element => { - const router = useRouter() - - return ( - <VStack - css={{ - mt: '10px', - width: '100%', - px: '10px', - py: '10px', - gap: '5px', - borderRadius: '5px', - '&:hover': { - bg: '#007AFF10', - cursor: 'pointer', - }, - '&:hover .title-text': { - textDecoration: 'underline', - }, - }} - onClick={(event) => { - const path = `/${props.viewerUsername ?? 'me'}/${props.homeItem.slug}` - if (event.metaKey || event.ctrlKey) { - window.open(path, '_blank') - } else { - router.push(path) - } - }} - > - <HStack - distribution="start" - alignment="center" - css={{ width: '100%', gap: '5px', lineHeight: '1' }} - > - <SourceInfo homeItem={props.homeItem} subtle={true} /> - - <SpanBox css={{ ml: 'auto', flexShrink: '0' }}> - <TimeAgo homeItem={props.homeItem} /> - </SpanBox> - </HStack> - <Title homeItem={props.homeItem} /> - <PreviewContent - previewContent={props.homeItem.previewContent} - maxLines="2" - /> - </VStack> - ) -} - -const SiteIconSmall = styled('img', { - width: '16px', - height: '16px', - borderRadius: '100px', -}) - -const SiteIconLarge = styled('img', { - width: '25px', - height: '25px', - borderRadius: '100px', -}) - -type SourceInfoProps = { - subtle?: boolean -} - -const SourceInfo = (props: HomeItemViewProps & SourceInfoProps) => ( - <HoverCard.Root> - <HoverCard.Trigger asChild> - <HStack - distribution="start" - alignment="center" - css={{ - gap: '8px', - height: '16px', - cursor: 'pointer', - flex: '1', - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {props.homeItem.source.icon && ( - <SiteIconSmall src={props.homeItem.source.icon} /> - )} - <HStack - css={{ - lineHeight: '1', - fontFamily: '$inter', - fontWeight: '500', - fontSize: props.subtle ? '12px' : '13px', - color: props.subtle ? '$homeTextSubtle' : '$homeTextSource', - textDecoration: 'underline', - }} - > - {props.homeItem.source.name} - </HStack> - </HStack> - </HoverCard.Trigger> - <HoverCard.Portal> - <HoverCard.Content sideOffset={5} style={{ zIndex: 5 }}> - <SubscriptionSourceHoverContent source={props.homeItem.source} /> - <HoverCard.Arrow fill={theme.colors.thBackground2.toString()} /> - </HoverCard.Content> - </HoverCard.Portal> - </HoverCard.Root> -) - -type SourceHoverContentProps = { - source: HomeItemSource -} - -const SubscriptionSourceHoverContent = ( - props: SourceHoverContentProps -): JSX.Element => { - const mapSourceType = ( - sourceType: HomeItemSourceType - ): SubscriptionType | undefined => { - switch (sourceType) { - case 'RSS': - case 'NEWSLETTER': - return sourceType as SubscriptionType - default: - return undefined - } - } - const { subscriptions, isValidating } = useGetSubscriptionsQuery( - mapSourceType(props.source.type) - ) - const subscription = useMemo(() => { - if (props.source.id && subscriptions) { - return subscriptions.find((sub) => sub.id == props.source.id) - } - return undefined - }, [subscriptions]) - - return ( - <VStack - alignment="start" - distribution="start" - css={{ - width: '380px', - height: '200px', - bg: '$thBackground2', - borderRadius: '10px', - padding: '15px', - gap: '10px', - boxShadow: theme.shadows.cardBoxShadow.toString(), - }} - > - <HStack - distribution="start" - alignment="center" - css={{ width: '100%', gap: '10px', height: '35px' }} - > - {props.source.icon && <SiteIconLarge src={props.source.icon} />} - <SpanBox - css={{ - fontFamily: '$inter', - fontWeight: '500', - fontSize: '14px', - }} - > - {props.source.name} - </SpanBox> - <SpanBox css={{ ml: 'auto', minWidth: '100px' }}> - {subscription && subscription.status == 'ACTIVE' && ( - <Button style="ctaSubtle" css={{ fontSize: '12px' }}> - Unsubscribe - </Button> - )} - </SpanBox> - </HStack> - <SpanBox - css={{ - fontFamily: '$inter', - fontSize: '13px', - color: '$homeTextBody', - }} - > - {subscription ? <>{subscription.description}</> : <></>} - </SpanBox> - </VStack> - ) -} diff --git a/packages/web/pages/l/[section].tsx b/packages/web/pages/l/[section].tsx index fc943e05b..782e7e14f 100644 --- a/packages/web/pages/l/[section].tsx +++ b/packages/web/pages/l/[section].tsx @@ -4,10 +4,10 @@ import { NavigationLayout, NavigationSection, } from '../../components/templates/NavigationLayout' -import { HomeContainer } from '../../components/nav-containers/home' +import { HomeContainer } from '../../components/nav-containers/HomeContainer' import { LibraryContainer } from '../../components/templates/library/LibraryContainer' import { useMemo } from 'react' -import { HighlightsContainer } from '../../components/nav-containers/highlights' +import { HighlightsContainer } from '../../components/nav-containers/HighlightsContainer' export default function Home(): JSX.Element { const router = useRouter()