Start to update navigation to use new sections

This commit is contained in:
Jackson Harper 2024-06-10 18:56:12 +08:00
parent 371ed24371
commit 883d6619ea
15 changed files with 346 additions and 175 deletions

View file

@ -23,7 +23,6 @@ import 'allotment/dist/style.css'
import { LibrarySideBar } from './library/LibrarySideBar'
export type NavigationSection =
| 'justread'
| 'home'
| 'library'
| 'subscriptions'

View file

@ -15,9 +15,9 @@ export function LibraryItemsContainer(): JSX.Element {
<Allotment.Pane minSize={200}>
<LibraryContainer />
</Allotment.Pane>
<Allotment.Pane snap maxSize={230}>
{/* <Allotment.Pane snap maxSize={230}>
<LibrarySideBar />
</Allotment.Pane>
</Allotment.Pane> */}
</Allotment>
)
}

View file

@ -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'

View file

@ -241,8 +241,8 @@ const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => {
<NavButton
{...props}
text="Home"
section="justread"
isSelected={props.section == 'justread'}
section="home"
isSelected={props.section == 'home'}
icon={<HomeIcon color={theme.colors.thHomeIcon.toString()} />}
/>
<NavButton
@ -385,7 +385,7 @@ async function setShortcuts(
): Promise<Shortcut[]> {
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<Shortcut[]> {
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',

View file

@ -88,9 +88,10 @@ export function apiPoster(
}
export function makePublicGqlFetcher(
gql: string,
variables?: unknown
): (query: string) => Promise<unknown> {
return (query: string) => gqlFetcher(query, variables, false)
return (query: string) => gqlFetcher(gql, variables, false)
}
// Partially apply gql variables to the request

View file

@ -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 }

View file

@ -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<PublicArticleAttributes> {
const result = (await ssrFetcher(context, query, input, false)) as PublicArticleData
if (result.sharedArticle.article) {
return result.sharedArticle.article
}
return Promise.reject()
}

View file

@ -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

View file

@ -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 (
<NavigationLayout
section="highlights"
pageMetaDataProps={{
title: 'Highlights',
path: '/highlights',
}}
>
<VStack
css={{
maxWidth: '70%',
padding: '20px',
margin: '30px 50px 0 0',
}}
>
{highlights.map((highlight) => {
return (
viewer.viewerData?.me && (
<HighlightCard
key={highlight.id}
highlight={highlight}
viewer={viewer.viewerData.me}
router={router}
mutate={mutate}
/>
)
)
})}
</VStack>
</NavigationLayout>
)
}
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 (
<HighlightViewNote
targetId={highlight.id}
text={annotation}
placeHolder="Add notes to this highlight..."
highlight={highlight}
mode={noteMode}
setEditMode={setNoteMode}
updateHighlight={(highlight) => {
setAnnotation(highlight.annotation)
}}
/>
)
}
function HighlightCard(props: HighlightCardProps): JSX.Element {
const [isOpen, setIsOpen] = useState(false)
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
useState<undefined | string>(undefined)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
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 (
<VStack
ref={refs.setReference}
{...getReferenceProps()}
css={{
width: '100%',
fontFamily: '$inter',
padding: '20px',
marginBottom: '20px',
bg: '$thBackground2',
borderRadius: '8px',
cursor: 'pointer',
'&:hover': {
backgroundColor: '$thBackground3',
},
}}
>
<Box
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
>
<HighlightHoverActions
viewer={props.viewer}
highlight={props.highlight}
isHovered={isOpen ?? false}
viewInReader={viewInReader}
setLabelsTarget={setLabelsTarget}
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
/>
</Box>
<Box
css={{
width: '30px',
height: '5px',
backgroundColor: highlightColor(props.highlight.color),
borderRadius: '2px',
}}
/>
<Box
css={{
color: '$thText',
fontSize: '11px',
marginTop: '10px',
fontWeight: 300,
}}
>
{timeAgo(props.highlight.updatedAt)}
</Box>
{props.highlight.quote && (
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{props.highlight.quote}
</ReactMarkdown>
)}
<HighlightAnnotation highlight={props.highlight} />
{props.highlight.labels && (
<HStack
css={{
marginBottom: '10px',
}}
>
{props.highlight.labels.map((label) => {
return (
<LabelChip key={label.id} color={label.color} text={label.name} />
)
})}
</HStack>
)}
<Box
css={{
color: '$thText',
fontSize: '12px',
lineHeight: '20px',
fontWeight: 300,
marginBottom: '10px',
}}
>
{props.highlight.libraryItem?.title}
</Box>
<Box
css={{
color: '$grayText',
fontSize: '12px',
lineHeight: '20px',
fontWeight: 300,
}}
>
{props.highlight.libraryItem?.author}
</Box>
{showConfirmDeleteHighlightId && (
<ConfirmationModal
message={'Are you sure you want to delete this highlight?'}
onAccept={() => {
;(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={
<TrashIcon
size={40}
color={theme.colors.grayTextContrast.toString()}
/>
}
/>
)}
{labelsTarget && (
<SetHighlightLabelsModalPresenter
highlight={labelsTarget}
highlightId={labelsTarget.id}
onUpdate={(highlight) => {
// Don't actually need to do something here
console.log('update highlight: ', highlight)
}}
onOpenChange={() => {
props.mutate()
setLabelsTarget(undefined)
}}
/>
)}
</VStack>
)
}

View file

@ -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 (
<NavigationLayout
section="highlights"
pageMetaDataProps={{
title: 'Highlights',
path: '/highlights',
}}
>
<VStack
alignment="start"
distribution="center"
css={{
px: '70px',
backgroundColor: '$thLibraryBackground',
'@lgDown': { px: '20px' },
'@mdDown': { px: '10px' },
}}
>
<div>Highlights will go here</div>
</VStack>
</NavigationLayout>
)
}

View file

@ -38,7 +38,7 @@ export default function Home(): JSX.Element {
useApplyLocalTheme()
return (
<NavigationLayout section="justread">
<NavigationLayout section="home">
<VStack
distribution="start"
alignment="center"

View file

@ -35,6 +35,7 @@ import { Button } from '../../components/elements/Button'
import { styled } from '@stitches/react'
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
import { escapeQuotes } from '../../utils/helper'
import { Shortcut } from '../../components/templates/navMenu/NavigationMenu'
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
const SHORTCUTS_KEY = 'library-shortcuts'
@ -318,6 +319,7 @@ const AvailableItems = (props: ListProps): JSX.Element => {
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: