From 883d6619ea11a45998883ff006db44d53b16ad69 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 10 Jun 2024 18:56:12 +0800 Subject: [PATCH] Start to update navigation to use new sections --- .../components/templates/NavigationLayout.tsx | 1 - .../library/LibraryItemsContainer.tsx | 4 +- .../templates/navMenu/LibraryMenu.tsx | 2 +- .../templates/navMenu/NavigationMenu.tsx | 8 +- packages/web/lib/networking/networkHelpers.ts | 3 +- .../queries/useGetLibraryItemsQuery.tsx | 2 +- .../queries/useGetPublicArticleQuery.tsx | 134 ------- .../queries/useValidateUsernameQuery.tsx | 3 +- .../{highlights.tsx => highlights-old.tsx} | 0 packages/web/pages/highlights/index.tsx | 329 ++++++++++++++++++ packages/web/pages/highlightsbak/index.tsx | 29 -- packages/web/pages/{home.tsx => home-old.tsx} | 0 .../web/pages/{justread => home}/debug.tsx | 0 .../web/pages/{justread => home}/index.tsx | 2 +- packages/web/pages/settings/shortcuts.tsx | 4 + 15 files changed, 346 insertions(+), 175 deletions(-) delete mode 100644 packages/web/lib/networking/queries/useGetPublicArticleQuery.tsx rename packages/web/pages/{highlights.tsx => highlights-old.tsx} (100%) create mode 100644 packages/web/pages/highlights/index.tsx delete mode 100644 packages/web/pages/highlightsbak/index.tsx rename packages/web/pages/{home.tsx => home-old.tsx} (100%) rename packages/web/pages/{justread => home}/debug.tsx (100%) rename packages/web/pages/{justread => home}/index.tsx (99%) diff --git a/packages/web/components/templates/NavigationLayout.tsx b/packages/web/components/templates/NavigationLayout.tsx index 0d843d3fa..fcf01b605 100644 --- a/packages/web/components/templates/NavigationLayout.tsx +++ b/packages/web/components/templates/NavigationLayout.tsx @@ -23,7 +23,6 @@ import 'allotment/dist/style.css' import { LibrarySideBar } from './library/LibrarySideBar' export type NavigationSection = - | 'justread' | 'home' | 'library' | 'subscriptions' diff --git a/packages/web/components/templates/library/LibraryItemsContainer.tsx b/packages/web/components/templates/library/LibraryItemsContainer.tsx index 26b806515..5e9c3f771 100644 --- a/packages/web/components/templates/library/LibraryItemsContainer.tsx +++ b/packages/web/components/templates/library/LibraryItemsContainer.tsx @@ -15,9 +15,9 @@ export function LibraryItemsContainer(): JSX.Element { - + {/* - + */} ) } diff --git a/packages/web/components/templates/navMenu/LibraryMenu.tsx b/packages/web/components/templates/navMenu/LibraryMenu.tsx index 7832c220e..23bc2dde4 100644 --- a/packages/web/components/templates/navMenu/LibraryMenu.tsx +++ b/packages/web/components/templates/navMenu/LibraryMenu.tsx @@ -25,7 +25,7 @@ import { HomeIcon } from '../../elements/icons/HomeIcon' import { LibraryIcon } from '../../elements/icons/LibraryIcon' import { HighlightsIcon } from '../../elements/icons/HighlightsIcon' import { CoverImage } from '../../elements/CoverImage' -import { Shortcut } from '../../../pages/settings/shortcuts' +import { Shortcut } from './NavigationMenu' import { OutlinedLabelChip } from '../../elements/OutlinedLabelChip' import { NewsletterIcon } from '../../elements/icons/NewsletterIcon' import { Dropdown, DropdownOption } from '../../elements/DropdownElements' diff --git a/packages/web/components/templates/navMenu/NavigationMenu.tsx b/packages/web/components/templates/navMenu/NavigationMenu.tsx index a3a3131c7..1154616df 100644 --- a/packages/web/components/templates/navMenu/NavigationMenu.tsx +++ b/packages/web/components/templates/navMenu/NavigationMenu.tsx @@ -241,8 +241,8 @@ const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => { } /> { const url = new URL(path, fetchEndpoint) try { - const response = await fetch(url, { + const response = await fetch(url.toString(), { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -409,7 +409,7 @@ async function setShortcuts( async function resetShortcuts(path: string): Promise { const url = new URL(path, fetchEndpoint) try { - const response = await fetch(url, { + const response = await fetch(url.toString(), { method: 'DELETE', headers: { 'Content-Type': 'application/json', diff --git a/packages/web/lib/networking/networkHelpers.ts b/packages/web/lib/networking/networkHelpers.ts index 8c3cd65e7..2b810fcee 100644 --- a/packages/web/lib/networking/networkHelpers.ts +++ b/packages/web/lib/networking/networkHelpers.ts @@ -88,9 +88,10 @@ export function apiPoster( } export function makePublicGqlFetcher( + gql: string, variables?: unknown ): (query: string) => Promise { - return (query: string) => gqlFetcher(query, variables, false) + return (query: string) => gqlFetcher(gql, variables, false) } // Partially apply gql variables to the request diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index a8699993d..325d57c39 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -256,7 +256,7 @@ export function useGetLibraryItemsQuery({ pageIndex === 0 ? undefined : previousResult.search.pageInfo.endCursor, ] }, - (_query, _l, _s, _sq, cursor) => { + (_query: string, _l: string, _s: string, _sq: string, cursor: string) => { return gqlFetcher(query, { ...variables, after: cursor }, true) }, { revalidateFirstPage: false } diff --git a/packages/web/lib/networking/queries/useGetPublicArticleQuery.tsx b/packages/web/lib/networking/queries/useGetPublicArticleQuery.tsx deleted file mode 100644 index 81a0e60e8..000000000 --- a/packages/web/lib/networking/queries/useGetPublicArticleQuery.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { gql } from 'graphql-request' -import useSWR from 'swr' -import { makePublicGqlFetcher, RequestContext, ssrFetcher } from '../networkHelpers' -import { Highlight } from '../fragments/highlightFragment' - -type PublicArticleQueryInput = { - username: string - slug: string - selectedHighlightId?: string -} - -export type PublicArticleQueryOutput = { - publicArticle?: PublicArticleAttributes - fetchError: unknown - isLoading: boolean - isValidating: boolean -} - -type PublicArticleData = { - sharedArticle: NestedPublicArticleData -} - -type NestedPublicArticleData = { - article: PublicArticleAttributes -} - -export type PublicArticleAttributes = { - id: string - title: string - slug: string - url: string - author?: string - image?: string - description?: string - hasContent?: boolean - highlights: Highlight[] -} - -export const PublicArticleGQLFragment = gql` - fragment PublicArticle on Article { - id - title - slug - url - author - image - description - savedByViewer - postedByViewer - hasContent - highlights { - id - shortId - quote - prefix - suffix - patch - annotation - sharedAt - user { - id - name - profile { - id - username - pictureUrl - } - } - } - } -` - -const query = gql` - query GetPublicArticle( - $username: String! - $slug: String! - $selectedHighlightId: String - ) { - sharedArticle( - username: $username - slug: $slug - selectedHighlightId: $selectedHighlightId - ) { - ... on SharedArticleSuccess { - article { - ...PublicArticle - } - } - - ... on SharedArticleError { - errorCodes - } - } - } - ${PublicArticleGQLFragment} -` - -export function useGetPublicArticleQuery({ - username, - slug, - selectedHighlightId, -}: PublicArticleQueryInput): PublicArticleQueryOutput { - const variables = { - username, - slug, - selectedHighlightId, - } - - const { data, error, isValidating } = useSWR( - // Only make request if username is defined - !!username ? [query, username, slug, selectedHighlightId] : null, - makePublicGqlFetcher(variables) - ) - const publicArticle = (data as PublicArticleData)?.sharedArticle?.article - - return { - publicArticle, - fetchError: error as unknown, - isLoading: !error && !publicArticle, - isValidating, - } -} - -export async function publicArticleQuery( - context: RequestContext, - input: PublicArticleQueryInput -): Promise { - const result = (await ssrFetcher(context, query, input, false)) as PublicArticleData - if (result.sharedArticle.article) { - return result.sharedArticle.article - } - - return Promise.reject() -} diff --git a/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx b/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx index 467d737fc..12fda73f7 100644 --- a/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx +++ b/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx @@ -24,7 +24,8 @@ export function useValidateUsernameQuery({ // Don't fetch if username is empty const { data, error, isValidating } = useSWR( username ? [query, username] : null, - makePublicGqlFetcher({ username }) + makePublicGqlFetcher(query, { username }), + {} ) // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/web/pages/highlights.tsx b/packages/web/pages/highlights-old.tsx similarity index 100% rename from packages/web/pages/highlights.tsx rename to packages/web/pages/highlights-old.tsx diff --git a/packages/web/pages/highlights/index.tsx b/packages/web/pages/highlights/index.tsx new file mode 100644 index 000000000..abd6619f4 --- /dev/null +++ b/packages/web/pages/highlights/index.tsx @@ -0,0 +1,329 @@ +import { NavigationLayout } from '../../components/templates/NavigationLayout' +import { Box, HStack, VStack } from '../../components/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 '../../components/templates/article/SetLabelsModalPresenter' +import { TrashIcon } from '../../components/elements/icons/TrashIcon' +import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' +import { ConfirmationModal } from '../../components/patterns/ConfirmationModal' +import { deleteHighlightMutation } from '../../lib/networking/mutations/deleteHighlightMutation' +import { LabelChip } from '../../components/elements/LabelChip' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { timeAgo } from '../../components/patterns/LibraryCards/LibraryCardStyles' +import { HighlightHoverActions } from '../../components/patterns/HighlightHoverActions' +import { + autoUpdate, + offset, + size, + useFloating, + useHover, + useInteractions, +} from '@floating-ui/react' +import { highlightColor } from '../../lib/themeUpdater' + +import { HighlightViewNote } from '../../components/patterns/HighlightNotes' + +const PAGE_SIZE = 10 + +export default function Highlights(): 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/pages/highlightsbak/index.tsx b/packages/web/pages/highlightsbak/index.tsx deleted file mode 100644 index f9e38bd1d..000000000 --- a/packages/web/pages/highlightsbak/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { NavigationLayout } from '../../components/templates/NavigationLayout' -import { PrimaryLayout } from '../../components/templates/PrimaryLayout' -import { HomeFeedContainer } from '../../components/templates/homeFeed/HomeFeedContainer' -import { VStack } from '../../components/elements/LayoutPrimitives' - -export default function Highlights(): JSX.Element { - return ( - - -
Highlights will go here
-
-
- ) -} diff --git a/packages/web/pages/home.tsx b/packages/web/pages/home-old.tsx similarity index 100% rename from packages/web/pages/home.tsx rename to packages/web/pages/home-old.tsx diff --git a/packages/web/pages/justread/debug.tsx b/packages/web/pages/home/debug.tsx similarity index 100% rename from packages/web/pages/justread/debug.tsx rename to packages/web/pages/home/debug.tsx diff --git a/packages/web/pages/justread/index.tsx b/packages/web/pages/home/index.tsx similarity index 99% rename from packages/web/pages/justread/index.tsx rename to packages/web/pages/home/index.tsx index a7edcb344..9a854acd3 100644 --- a/packages/web/pages/justread/index.tsx +++ b/packages/web/pages/home/index.tsx @@ -38,7 +38,7 @@ export default function Home(): JSX.Element { useApplyLocalTheme() return ( - + { id: search.id, name: search.name, type: 'label', + section: 'library', filter: search.filter, } props.dispatchList({ @@ -364,6 +366,7 @@ const AvailableItems = (props: ListProps): JSX.Element => { id: label.id, type: 'label', label: label, + section: 'library', name: label.name, filter: `label:\"${escapeQuotes(label.name)}\"`, } @@ -408,6 +411,7 @@ const AvailableItems = (props: ListProps): JSX.Element => { onClick={(event) => { const item: Shortcut = { id: subscription.id, + section: 'subscriptions', name: subscription.name, icon: subscription.icon, type: