Add checkboxes on library cards in multi select mode

This commit is contained in:
Jackson Harper 2023-06-07 21:16:48 +08:00
parent e6eadf75f0
commit d4aa715b5b
5 changed files with 283 additions and 257 deletions

View file

@ -2,39 +2,21 @@ import React from 'react'
import { styled } from '@stitches/react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { CheckIcon } from './images/CheckIcon'
import { Box } from './LayoutPrimitives'
import { Check } from 'phosphor-react'
// const Checkbox = styled(CheckboxPrimitive.Root, {
// all: 'unset',
// width: 15,
// height: 15,
// borderRadius: 2,
// display: 'flex',
// alignItems: 'center',
// justifyContent: 'center',
// })
// const CheckboxIndicator = styled(CheckboxPrimitive.Indicator, {
// color: '#FFFFFF',
// })
const CheckboxRoot = styled(CheckboxPrimitive.Root, {
const Checkbox = styled(CheckboxPrimitive.Root, {
all: 'unset',
backgroundColor: 'white',
width: 25,
height: 25,
borderRadius: 4,
width: 16,
height: 16,
minWidth: 16,
minHeight: 16,
borderRadius: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: `0 2px 10px black`,
'&:hover': { backgroundColor: 'violet' },
'&:focus': { boxShadow: `0 0 0 2px black` },
})
const CheckboxIndicator = styled(CheckboxPrimitive.Indicator, {
color: 'violet',
color: '#FFFFFF',
})
export const CheckboxComponent: React.FC<{
@ -44,15 +26,18 @@ export const CheckboxComponent: React.FC<{
const toggleChecked = () => setChecked(!checked)
return (
<form>
<Box css={{ alignItems: 'center' }}>
<CheckboxRoot defaultChecked id="c1">
<CheckboxIndicator>
<Check size={20} color="black" />
</CheckboxIndicator>
</CheckboxRoot>
</Box>
</form>
<Checkbox
css={{
border: checked ? '2px solid #F9D354' : '2px solid #3F3E3C4D',
backgroundColor: checked ? '#F9D354' : '#FFFFFF',
}}
checked={checked}
onCheckedChange={toggleChecked}
>
<CheckboxIndicator>
<CheckIcon />
</CheckboxIndicator>
</Checkbox>
)
}

View file

@ -21,4 +21,6 @@ export type LinkedItemCardProps = {
viewer: UserBasicData
inMultiSelect: boolean
handleAction: (action: LinkedItemCardAction) => void
isHovered?: boolean
}

View file

@ -146,3 +146,18 @@ export function LibraryItemMetadata(
</Box>
)
}
type CardCheckBoxProps = {
isChecked: boolean
handleChanged: () => void
}
export function CardCheckbox(props: CardCheckBoxProps): JSX.Element {
return (
<input
type="checkbox"
checked={props.isChecked}
onChange={props.handleChanged}
></input>
)
}

View file

@ -4,22 +4,21 @@ import type { LinkedItemCardProps } from './CardTypes'
import { CoverImage } from '../../elements/CoverImage'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { DotsThreeVertical } from 'phosphor-react'
import Link from 'next/link'
import { CardMenu } from '../CardMenu'
import {
AuthorInfoStyle,
CardCheckbox,
DescriptionStyle,
LibraryItemMetadata,
MenuStyle,
MetaStyle,
siteName,
timeAgo,
TitleStyle,
} from './LibraryCardStyles'
import { sortedLabels } from '../../../lib/labelsSort'
import CheckboxComponent from '../../elements/Checkbox'
dayjs.extend(relativeTime)
@ -53,12 +52,8 @@ export function ProgressBar(props: ProgressBarProps): JSX.Element {
)
}
// Component
export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const originText = siteName(props.item.originalArticleUrl, props.item.url)
return (
<VStack
@ -88,145 +83,156 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
setIsHovered(false)
}}
>
<Link
href={`${props.viewer.profile.username}/${props.item.slug}`}
passHref
>
<a
{props.inMultiSelect ? (
<LibraryGridCardContent {...props} isHovered={isHovered} />
) : (
<Link
href={`${props.viewer.profile.username}/${props.item.slug}`}
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
tabIndex={-1}
passHref
>
<HStack
css={{
...MetaStyle,
minHeight: '35px',
}}
distribution="start"
<a
href={`${props.viewer.profile.username}/${props.item.slug}`}
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
tabIndex={-1}
>
<LibraryItemMetadata item={props.item} />
{props.inMultiSelect ? (
<SpanBox css={{ marginLeft: 'auto' }}>
<input
type="checkbox"
onClick={(event) => {
event.preventDefault()
}}
></input>
</SpanBox>
) : (
<Box
css={{
...MenuStyle,
visibility: isHovered || menuOpen ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<CardMenu
item={props.item}
viewer={props.viewer}
onOpenChange={(open) => setMenuOpen(open)}
actionHandler={props.handleAction}
triggerElement={
<DotsThreeVertical
size={25}
weight="bold"
color="#ADADAD"
/>
}
/>
</Box>
)}
</HStack>
<VStack
alignment="start"
distribution="start"
css={{ height: '100%', width: '100%' }}
>
<Box
css={{
...TitleStyle,
height: '42px',
}}
>
{props.item.title}
</Box>
<Box css={DescriptionStyle}>{props.item.description}</Box>
<SpanBox
css={{
...AuthorInfoStyle,
mt: '10px',
}}
>
{props.item.author}
{props.item.author && originText && ' | '}
<SpanBox css={{ textDecoration: 'underline' }}>
{originText}
</SpanBox>
</SpanBox>
<SpanBox
css={{
pt: '20px',
pr: '10px',
pb: '20px',
width: '100%',
m: '0px',
}}
>
<ProgressBar
fillPercentage={props.item.readingProgressPercent}
fillColor="$thProgressFg"
backgroundColor="$thBorderSubtle"
borderRadius="5px"
/>
</SpanBox>
<HStack
distribution="start"
alignment="start"
css={{ width: '100%', minHeight: '50px' }}
>
<HStack
css={{
display: 'block',
minHeight: '35px',
}}
>
{sortedLabels(props.item.labels).map(
({ name, color }, index) => (
<LabelChip key={index} text={name || ''} color={color} />
)
)}
</HStack>
<VStack
css={{
width: '80px',
height: '100%',
marginLeft: 'auto',
flexGrow: '1',
}}
alignment="end"
distribution="end"
>
{props.item.image && (
<CoverImage
src={props.item.image}
alt="Link Preview Image"
width={50}
height={50}
css={{ borderRadius: '8px' }}
onError={(e) => {
;(e.target as HTMLElement).style.display = 'none'
}}
/>
)}
</VStack>
</HStack>
</VStack>
</a>
</Link>
<LibraryGridCardContent {...props} isHovered={isHovered} />
</a>
</Link>
)}
</VStack>
)
}
const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => {
const [menuOpen, setMenuOpen] = useState(false)
const [isChecked, setIsChecked] = useState(false)
const originText = siteName(props.item.originalArticleUrl, props.item.url)
const handleCheckChanged = useCallback(() => {
setIsChecked(!isChecked)
}, [isChecked])
return (
<>
<HStack
css={{
...MetaStyle,
minHeight: '35px',
}}
distribution="start"
>
<LibraryItemMetadata item={props.item} />
{props.inMultiSelect ? (
<SpanBox css={{ marginLeft: 'auto' }}>
<CardCheckbox
isChecked={isChecked}
handleChanged={handleCheckChanged}
/>
</SpanBox>
) : (
<Box
css={{
...MenuStyle,
visibility: props.isHovered || menuOpen ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<CardMenu
item={props.item}
viewer={props.viewer}
onOpenChange={(open) => setMenuOpen(open)}
actionHandler={props.handleAction}
triggerElement={
<DotsThreeVertical size={25} weight="bold" color="#ADADAD" />
}
/>
</Box>
)}
</HStack>
<VStack
alignment="start"
distribution="start"
css={{ height: '100%', width: '100%' }}
>
<Box
css={{
...TitleStyle,
height: '42px',
}}
>
{props.item.title}
</Box>
<Box css={DescriptionStyle}>{props.item.description}</Box>
<SpanBox
css={{
...AuthorInfoStyle,
mt: '10px',
}}
>
{props.item.author}
{props.item.author && originText && ' | '}
<SpanBox css={{ textDecoration: 'underline' }}>{originText}</SpanBox>
</SpanBox>
<SpanBox
css={{
pt: '20px',
pr: '10px',
pb: '20px',
width: '100%',
m: '0px',
}}
>
<ProgressBar
fillPercentage={props.item.readingProgressPercent}
fillColor="$thProgressFg"
backgroundColor="$thBorderSubtle"
borderRadius="5px"
/>
</SpanBox>
<HStack
distribution="start"
alignment="start"
css={{ width: '100%', minHeight: '50px' }}
>
<HStack
css={{
display: 'block',
minHeight: '35px',
}}
>
{sortedLabels(props.item.labels).map(({ name, color }, index) => (
<LabelChip key={index} text={name || ''} color={color} />
))}
</HStack>
<VStack
css={{
width: '80px',
height: '100%',
marginLeft: 'auto',
flexGrow: '1',
}}
alignment="end"
distribution="end"
>
{props.item.image && (
<CoverImage
src={props.item.image}
alt="Link Preview Image"
width={50}
height={50}
css={{ borderRadius: '8px' }}
onError={(e) => {
;(e.target as HTMLElement).style.display = 'none'
}}
/>
)}
</VStack>
</HStack>
</VStack>
</>
)
}

View file

@ -1,27 +1,23 @@
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { LabelChip } from '../../elements/LabelChip'
import type { LinkedItemCardProps } from './CardTypes'
import { useMemo, useState } from 'react'
import { useCallback, useState } from 'react'
import { DotsThree } from 'phosphor-react'
import Link from 'next/link'
import { CardMenu } from '../CardMenu'
import {
AuthorInfoStyle,
CardCheckbox,
LibraryItemMetadata,
MenuStyle,
MetaStyle,
siteName,
timeAgo,
TitleStyle,
} from './LibraryCardStyles'
import { sortedLabels } from '../../../lib/labelsSort'
import { Checkbox } from '@radix-ui/react-checkbox'
export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const originText = siteName(props.item.originalArticleUrl, props.item.url)
return (
<VStack
@ -54,83 +50,105 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
setIsHovered(false)
}}
>
<Link
href={`${props.viewer.profile.username}/${props.item.slug}`}
passHref
>
<a
{props.inMultiSelect ? (
<LibraryListCardContent {...props} isHovered={isHovered} />
) : (
<Link
href={`${props.viewer.profile.username}/${props.item.slug}`}
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
tabIndex={-1}
passHref
>
<HStack css={MetaStyle} distribution="start">
<LibraryItemMetadata item={props.item} showProgress={true} />
{props.inMultiSelect ? (
<SpanBox css={{ marginLeft: 'auto' }}>
<input type="checkbox"></input>
</SpanBox>
) : (
<Box
css={{
...MenuStyle,
visibility: isHovered || menuOpen ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<CardMenu
item={props.item}
viewer={props.viewer}
onOpenChange={(open) => setMenuOpen(open)}
actionHandler={props.handleAction}
triggerElement={
<DotsThree size={25} weight="bold" color="#ADADAD" />
}
/>
</Box>
)}
</HStack>
<VStack
alignment="start"
distribution="start"
css={{ height: '100%', width: '100%' }}
<a
href={`${props.viewer.profile.username}/${props.item.slug}`}
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
tabIndex={-1}
>
<Box css={{ ...TitleStyle, width: '80%' }}>{props.item.title}</Box>
<SpanBox
css={{
mt: '5px',
...AuthorInfoStyle,
}}
>
{props.item.author}
{props.item.author && originText && ' | '}
<SpanBox css={{ textDecoration: 'underline' }}>
{originText}
</SpanBox>
</SpanBox>
<HStack
distribution="start"
alignment="start"
// The two pixels here are to account for the label margin
css={{ width: '100%', ml: '-2px', mt: '5px' }}
>
<HStack
css={{
display: 'block',
}}
>
{sortedLabels(props.item.labels).map(
({ name, color }, index) => (
<LabelChip key={index} text={name || ''} color={color} />
)
)}
</HStack>
</HStack>
</VStack>
</a>
</Link>
<LibraryListCardContent {...props} isHovered={isHovered} />
</a>
</Link>
)}
</VStack>
)
}
export function LibraryListCardContent(
props: LinkedItemCardProps
): JSX.Element {
const [menuOpen, setMenuOpen] = useState(false)
const [isChecked, setIsChecked] = useState(false)
const originText = siteName(props.item.originalArticleUrl, props.item.url)
const handleCheckChanged = useCallback(() => {
setIsChecked(!isChecked)
}, [isChecked])
return (
<>
<HStack css={MetaStyle} distribution="start">
<LibraryItemMetadata item={props.item} showProgress={true} />
{props.inMultiSelect ? (
<SpanBox css={{ marginLeft: 'auto' }}>
<CardCheckbox
isChecked={isChecked}
handleChanged={handleCheckChanged}
/>
</SpanBox>
) : (
<Box
css={{
...MenuStyle,
visibility: props.isHovered || menuOpen ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<CardMenu
item={props.item}
viewer={props.viewer}
onOpenChange={(open) => setMenuOpen(open)}
actionHandler={props.handleAction}
triggerElement={
<DotsThree size={25} weight="bold" color="#ADADAD" />
}
/>
</Box>
)}
</HStack>
<VStack
alignment="start"
distribution="start"
css={{ height: '100%', width: '100%' }}
>
<Box css={{ ...TitleStyle, width: '80%' }}>{props.item.title}</Box>
<SpanBox
css={{
mt: '5px',
...AuthorInfoStyle,
}}
>
{props.item.author}
{props.item.author && originText && ' | '}
<SpanBox css={{ textDecoration: 'underline' }}>{originText}</SpanBox>
</SpanBox>
<HStack
distribution="start"
alignment="start"
// The two pixels here are to account for the label margin
css={{ width: '100%', ml: '-2px', mt: '5px' }}
>
<HStack
css={{
display: 'block',
}}
>
{sortedLabels(props.item.labels).map(({ name, color }, index) => (
<LabelChip key={index} text={name || ''} color={color} />
))}
</HStack>
</HStack>
</VStack>
</>
)
}