mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Rename web container files
This commit is contained in:
parent
bc0eeba158
commit
80bded24db
3 changed files with 2 additions and 1208 deletions
|
|
@ -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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
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',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<VStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '100%',
|
||||
bg: '$readerBg',
|
||||
pt: '45px',
|
||||
minHeight: '100vh',
|
||||
minWidth: '320px',
|
||||
'@mdDown': {
|
||||
pt: '0px',
|
||||
mt: '80px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toaster />
|
||||
<VStack
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '680px',
|
||||
gap: '50px',
|
||||
minHeight: '100vh',
|
||||
'@mdDown': {
|
||||
gap: '40px',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{homeData.sections?.map((homeSection, idx) => {
|
||||
if (homeSection.items.length < 1) {
|
||||
console.log('empty home section: ', homeSection)
|
||||
return <SpanBox key={`section-${idx}`}></SpanBox>
|
||||
}
|
||||
switch (homeSection.layout) {
|
||||
case 'just_added':
|
||||
return (
|
||||
<JustAddedHomeSection
|
||||
key={`section-${idx}`}
|
||||
homeSection={homeSection}
|
||||
viewerUsername={viewerUsername}
|
||||
/>
|
||||
)
|
||||
case 'top_picks':
|
||||
return (
|
||||
<TopPicksHomeSection
|
||||
key={`section-${idx}`}
|
||||
homeSection={homeSection}
|
||||
viewerUsername={viewerUsername}
|
||||
/>
|
||||
)
|
||||
case 'quick_links':
|
||||
return (
|
||||
<QuickLinksHomeSection
|
||||
key={`section-${idx}`}
|
||||
homeSection={homeSection}
|
||||
viewerUsername={viewerUsername}
|
||||
/>
|
||||
)
|
||||
case 'hidden':
|
||||
return (
|
||||
<HiddenHomeSection
|
||||
key={`section-${idx}`}
|
||||
homeSection={homeSection}
|
||||
viewerUsername={viewerUsername}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
console.log('unknown home section: ', homeSection)
|
||||
return <SpanBox key={`section-${idx}`}></SpanBox>
|
||||
}
|
||||
})}
|
||||
</VStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type HomeSectionProps = {
|
||||
homeSection: HomeSection
|
||||
viewerUsername: string | undefined
|
||||
}
|
||||
|
||||
const JustAddedHomeSection = (props: HomeSectionProps): JSX.Element => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<VStack
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
gap: '20px',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
lineHeight: '1',
|
||||
'@mdDown': {
|
||||
px: '20px',
|
||||
},
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
fontFamily: '$inter',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '$homeTextTitle',
|
||||
}}
|
||||
>
|
||||
{props.homeSection.title}
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
ml: 'auto',
|
||||
fontFamily: '$inter',
|
||||
fontSize: '13px',
|
||||
fontWeight: '400',
|
||||
color: '$homeTextTitle',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="link"
|
||||
onClick={(event) => {
|
||||
router.push('/l/library')
|
||||
event.preventDefault()
|
||||
}}
|
||||
css={{
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
lineHeight: '1',
|
||||
overflowX: 'scroll',
|
||||
gap: '25px',
|
||||
scrollbarWidth: 'none',
|
||||
'::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
'@mdDown': {
|
||||
px: '20px',
|
||||
},
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
{props.homeSection.items.map((homeItem) => {
|
||||
return <JustAddedItemView key={homeItem.id} homeItem={homeItem} />
|
||||
})}
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<VStack
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
gap: '20px',
|
||||
'@mdDown': {
|
||||
gap: '10px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{items.length > 0 && (
|
||||
<SpanBox
|
||||
css={{
|
||||
fontFamily: '$inter',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '$homeTextTitle',
|
||||
'@mdDown': {
|
||||
px: '20px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.homeSection.title}
|
||||
</SpanBox>
|
||||
)}
|
||||
|
||||
<Pagination
|
||||
items={items}
|
||||
itemsPerPage={10}
|
||||
loadMoreButtonText="Load more Top Picks"
|
||||
render={(homeItem) => (
|
||||
<TopPicksItemView
|
||||
key={homeItem.id}
|
||||
homeItem={homeItem}
|
||||
dispatchList={dispatchList}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const QuickLinksHomeSection = (props: HomeSectionProps): JSX.Element => {
|
||||
return (
|
||||
<VStack
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
gap: '10px',
|
||||
bg: '$homeCardHover',
|
||||
py: '20px',
|
||||
px: '20px',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
fontFamily: '$inter',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
textTransform: 'uppercase',
|
||||
color: '$ctaBlue',
|
||||
bg: '#007AFF20',
|
||||
px: '10px',
|
||||
py: '5px',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
{props.homeSection.title}
|
||||
</SpanBox>
|
||||
|
||||
<Pagination
|
||||
items={props.homeSection.items}
|
||||
itemsPerPage={8}
|
||||
render={(homeItem) => (
|
||||
<QuickLinkHomeItemView key={homeItem.id} homeItem={homeItem} />
|
||||
)}
|
||||
/>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const HiddenHomeSection = (props: HomeSectionProps): JSX.Element => {
|
||||
const [isHidden, setIsHidden] = useState(true)
|
||||
return (
|
||||
<VStack
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
gap: '20px',
|
||||
marginBottom: '40px',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
gap: '10px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setIsHidden(!isHidden)}
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
fontFamily: '$inter',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '$homeTextTitle',
|
||||
}}
|
||||
>
|
||||
{props.homeSection.title}
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
fontFamily: '$inter',
|
||||
fontSize: '13px',
|
||||
color: '$readerFont',
|
||||
}}
|
||||
>
|
||||
{isHidden ? 'Show' : 'Hide'}
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
|
||||
{isHidden ? <></> : <HiddenHomeSectionView />}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const HiddenHomeSectionView = (): JSX.Element => {
|
||||
const hiddenSectionData = useGetHiddenHomeSection()
|
||||
|
||||
if (hiddenSectionData.error) {
|
||||
return <SpanBox>Error loading hidden section</SpanBox>
|
||||
}
|
||||
|
||||
if (hiddenSectionData.isValidating) {
|
||||
return <SpanBox>Loading...</SpanBox>
|
||||
}
|
||||
|
||||
if (!hiddenSectionData.section) {
|
||||
return <SpanBox>No hidden section data</SpanBox>
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{hiddenSectionData.section.items.map((homeItem) => {
|
||||
return <QuickLinkHomeItemView key={homeItem.id} homeItem={homeItem} />
|
||||
})}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const CoverImage = styled('img', {
|
||||
objectFit: 'cover',
|
||||
})
|
||||
|
||||
type HomeItemViewProps = {
|
||||
homeItem: HomeItem
|
||||
viewerUsername?: string | undefined
|
||||
}
|
||||
|
||||
const TimeAgo = (props: HomeItemViewProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 'medium',
|
||||
fontFamily: '$inter',
|
||||
color: '$homeTextSubtle',
|
||||
flexShrink: '0',
|
||||
}}
|
||||
>
|
||||
{timeAgo(props.homeItem.date)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const Title = (props: HomeItemViewProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
className="title-text"
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
mb: '6px',
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
fontWeight: '600',
|
||||
fontFamily: '$inter',
|
||||
color: '$homeTextTitle',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
display: '-webkit-box',
|
||||
'-webkit-line-clamp': '3',
|
||||
'-webkit-box-orient': 'vertical',
|
||||
'&:title-text': {
|
||||
transition: 'text-decoration 0.3s ease',
|
||||
},
|
||||
'@mdDown': {
|
||||
fontSize: '16px',
|
||||
lineHeight: '20px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.homeItem.title}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const TitleSmall = (props: HomeItemViewProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
className="title-text"
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
fontSize: '14px',
|
||||
lineHeight: '21px',
|
||||
minHeight: '42px', // always have two lines of space
|
||||
fontWeight: '500',
|
||||
fontFamily: '$inter',
|
||||
color: '$homeTextTitle',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
display: '-webkit-box',
|
||||
'-webkit-line-clamp': '3',
|
||||
'-webkit-box-orient': 'vertical',
|
||||
}}
|
||||
>
|
||||
{props.homeItem.title}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type PreviewContentProps = {
|
||||
previewContent?: string
|
||||
maxLines?: string
|
||||
}
|
||||
|
||||
const PreviewContent = (props: PreviewContentProps): JSX.Element => {
|
||||
return (
|
||||
<SpanBox
|
||||
css={{
|
||||
fontFamily: '$inter',
|
||||
fontSize: '14px',
|
||||
lineHeight: '21px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
display: '-webkit-box',
|
||||
'-webkit-line-clamp': props.maxLines ?? '3',
|
||||
'-webkit-box-orient': 'vertical',
|
||||
'@mdDown': {
|
||||
'-webkit-line-clamp': '3',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.previewContent ?? ''}
|
||||
</SpanBox>
|
||||
)
|
||||
}
|
||||
|
||||
const JustAddedItemView = (props: HomeItemViewProps): JSX.Element => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
minWidth: '377px',
|
||||
gap: '5px',
|
||||
padding: '12px',
|
||||
cursor: 'pointer',
|
||||
bg: '$homeCardHover',
|
||||
borderRadius: '5px',
|
||||
'&:hover': {
|
||||
bg: '$homeCardHover',
|
||||
},
|
||||
'&:hover .title-text': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
'@mdDown': {
|
||||
minWidth: '282px',
|
||||
},
|
||||
}}
|
||||
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>
|
||||
|
||||
<TitleSmall homeItem={props.homeItem} />
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type TopPicksItemViewProps = {
|
||||
dispatchList: (args: { type: string; itemId?: string }) => void
|
||||
}
|
||||
|
||||
const TopPicksItemView = (
|
||||
props: HomeItemViewProps & TopPicksItemViewProps
|
||||
): JSX.Element => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
width: '100%',
|
||||
pt: '15px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '5px',
|
||||
'@mdDown': {
|
||||
borderRadius: '0px',
|
||||
},
|
||||
'&:hover': {
|
||||
bg: '$homeCardHover',
|
||||
},
|
||||
'&: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)
|
||||
}
|
||||
}}
|
||||
alignment="start"
|
||||
>
|
||||
<Box css={{ width: '100%', gap: '10px', px: '20px' }}>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ gap: '5px', lineHeight: '1', mb: '10px' }}
|
||||
>
|
||||
<SourceInfo homeItem={props.homeItem} />
|
||||
<SpanBox css={{ '@mdDown': { ml: 'auto' } }}>
|
||||
<TimeAgo homeItem={props.homeItem} />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
|
||||
{props.homeItem.thumbnail && (
|
||||
<CoverImage
|
||||
css={{
|
||||
width: '120px',
|
||||
height: '70px',
|
||||
borderRadius: '4px',
|
||||
marginLeft: '10px',
|
||||
float: 'right',
|
||||
}}
|
||||
src={props.homeItem.thumbnail}
|
||||
></CoverImage>
|
||||
)}
|
||||
<Title homeItem={props.homeItem} />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue