mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add new mobile highlight cards
This commit is contained in:
parent
3adb779230
commit
3fdb7f8694
2 changed files with 391 additions and 0 deletions
|
|
@ -0,0 +1,354 @@
|
|||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
SpanBox,
|
||||
Blockquote,
|
||||
} from '../../elements/LayoutPrimitives'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import type { LinkedItemCardProps } from './CardTypes'
|
||||
import { CoverImage } from '../../elements/CoverImage'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import {
|
||||
CaretCircleDown,
|
||||
CaretDown,
|
||||
CaretUp,
|
||||
DotsThreeVertical,
|
||||
} from 'phosphor-react'
|
||||
import Link from 'next/link'
|
||||
import { CardMenu } from '../CardMenu'
|
||||
import {
|
||||
AuthorInfoStyle,
|
||||
DescriptionStyle,
|
||||
MenuStyle,
|
||||
MetaStyle,
|
||||
TitleStyle,
|
||||
} from './LibraryCardStyles'
|
||||
import { Separator } from '../../elements/Separator'
|
||||
import { styled } from '@stitches/react'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import {
|
||||
LibraryItem,
|
||||
LibraryItemNode,
|
||||
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const timeAgo = (date: string | undefined): string => {
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
return dayjs(date).fromNow()
|
||||
}
|
||||
|
||||
export const GridSeparator = styled(Box, {
|
||||
height: '1px',
|
||||
marginTop: '15px',
|
||||
backgroundColor: '$thBorderColor',
|
||||
})
|
||||
|
||||
type LibraryHighlightGridCardProps = {
|
||||
viewer: UserBasicData
|
||||
item: LibraryItemNode
|
||||
}
|
||||
|
||||
// Component
|
||||
export function LibraryHighlightGridCard(
|
||||
props: LibraryHighlightGridCardProps
|
||||
): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const higlightCount = props.item.highlights?.length ?? 0
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
pl: '15px',
|
||||
padding: '15px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
marginTop: '20px',
|
||||
background: 'white',
|
||||
borderRadius: '5px',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: '$thBorderColor',
|
||||
cursor: 'pointer',
|
||||
bg: '$thBackground3',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
{!expanded && (
|
||||
<HStack
|
||||
css={{
|
||||
...MetaStyle,
|
||||
minHeight: '35px',
|
||||
}}
|
||||
distribution="start"
|
||||
>
|
||||
<Box>
|
||||
{timeAgo(props.item.savedAt)}
|
||||
{` `}
|
||||
{props.item.wordsCount ?? 0 > 0
|
||||
? ` • ${Math.max(
|
||||
1,
|
||||
Math.round((props.item.wordsCount ?? 0) / 235)
|
||||
)} min read`
|
||||
: null}
|
||||
{props.item.highlights?.length ?? 0 > 0
|
||||
? ` • ${props.item.highlights?.length} highlights`
|
||||
: null}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
...TitleStyle,
|
||||
}}
|
||||
>
|
||||
{props.item.title}
|
||||
</Box>
|
||||
{expanded && (
|
||||
<>
|
||||
<GridSeparator css={{ width: '100%' }} />
|
||||
<VStack
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
distribution="start"
|
||||
>
|
||||
{(props.item.highlights ?? []).map((highlight) => (
|
||||
<HighlightItemCard
|
||||
key={highlight.id}
|
||||
viewer={props.viewer}
|
||||
item={props.item}
|
||||
highlight={highlight}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</>
|
||||
)}
|
||||
<Box css={{ width: '100%' }}>
|
||||
{!expanded ? (
|
||||
<Button
|
||||
css={{
|
||||
mt: '30px',
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
px: '10px',
|
||||
py: '6px',
|
||||
fontSize: '11px',
|
||||
fontFamily: '$inter',
|
||||
fontWeight: '500',
|
||||
bg: '$thBackground2',
|
||||
color: '$thTextSubtle2',
|
||||
alignItems: 'center',
|
||||
border: '1px solid transparent',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
setExpanded(true)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{`View ${higlightCount} highlight${higlightCount > 1 ? 's' : ''}`}
|
||||
<CaretDown
|
||||
size={10}
|
||||
weight="bold"
|
||||
color={theme.colors.thHighContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
width: '25px',
|
||||
height: '25px',
|
||||
bg: '$thBackground2',
|
||||
pt: '2px',
|
||||
borderRadius: '1000px',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
setExpanded(false)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<CaretUp size={15} weight="bold" color="#EC6A5E" />
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</VStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightItemCardProps = {
|
||||
highlight: Highlight
|
||||
viewer: UserBasicData | undefined
|
||||
item: LibraryItemNode
|
||||
}
|
||||
|
||||
const StyledQuote = styled(Blockquote, {
|
||||
margin: '0px',
|
||||
fontSize: '16px',
|
||||
fontFamily: '$inter',
|
||||
lineHeight: '1.50',
|
||||
color: '$thHighContrast',
|
||||
paddingLeft: '15px',
|
||||
borderLeft: '2px solid $omnivoreCtaYellow',
|
||||
})
|
||||
|
||||
function HighlightItemCard(props: HighlightItemCardProps): 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]
|
||||
)
|
||||
|
||||
return (
|
||||
<HStack
|
||||
css={{ width: '100%', py: '20px', cursor: 'pointer' }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'clip',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
onClick={(event) => {
|
||||
if (router && props.viewer) {
|
||||
const dest = `/${props.viewer}/${props.item.slug}#${props.highlight.id}`
|
||||
router.push(dest)
|
||||
}
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<StyledQuote>
|
||||
<SpanBox css={{ p: '1px', borderRadius: '2px' }}>
|
||||
{lines.map((line: string, index: number) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
{index !== lines.length - 1 && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</SpanBox>
|
||||
</StyledQuote>
|
||||
|
||||
<Box css={{ display: 'block', pt: '16px' }}>
|
||||
{props.highlight.labels?.map((label: Label, index: number) => (
|
||||
<LabelChip
|
||||
key={index}
|
||||
text={label.name || ''}
|
||||
color={label.color}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<StyledText
|
||||
css={{
|
||||
borderRadius: '6px',
|
||||
bg: '$thBackground4',
|
||||
p: '10px',
|
||||
width: '100%',
|
||||
marginTop: '5px',
|
||||
color: '$grayText',
|
||||
}}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{props.highlight.annotation
|
||||
? props.highlight.annotation
|
||||
: 'Add your notes...'}
|
||||
</StyledText>
|
||||
</VStack>
|
||||
<SpanBox
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
width: '20px',
|
||||
visibility: hover ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HighlightsMenu />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
function HighlightsMenu(): JSX.Element {
|
||||
return (
|
||||
<Dropdown
|
||||
triggerElement={
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
'&:hover': {
|
||||
bg: '#898989',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DotsThreeVertical size={20} color="#EBEBEB" weight="bold" />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
console.log('copy')
|
||||
}}
|
||||
title="Copy"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
console.log('labels')
|
||||
}}
|
||||
title="Labels"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
console.log('delete')
|
||||
}}
|
||||
title="Delete"
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
maxWidth: '330px',
|
||||
// height: '48px',
|
||||
position: 'absolute',
|
||||
background: '$grayBg',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid $grayBorder',
|
||||
boxShadow: theme.shadows.cardBoxShadow.toString(),
|
||||
left: props.anchorCoordinates.pageX,
|
||||
top: props.anchorCoordinates.pageY,
|
||||
}}
|
||||
>
|
||||
<HighlightView highlight={props.highlight} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue