webapp/src/components/Feed/ArticleCard/ArticleCard.tsx

370 lines
14 KiB
TypeScript
Raw Normal View History

2023-11-28 13:18:25 +00:00
import type { Shout } from '../../../graphql/schema/core.gen'
import { getPagePath, openPage } from '@nanostores/router'
2022-10-19 14:26:49 +00:00
import { clsx } from 'clsx'
import { createMemo, createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { router, useRouter } from '../../../stores/router'
import { capitalize } from '../../../utils/capitalize'
import { getDescription } from '../../../utils/meta'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'
import { Popover } from '../../_shared/Popover'
import { CoverImage } from '../../Article/CoverImage'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { AuthorLink } from '../../Author/AhtorLink'
import { CardTopic } from '../CardTopic'
import { FeedArticlePopup } from '../FeedArticlePopup'
import styles from './ArticleCard.module.scss'
import stylesHeader from '../../Nav/Header/Header.module.scss'
2022-09-09 11:53:35 +00:00
export type ArticleCardProps = {
// TODO: refactor this, please
2022-09-09 11:53:35 +00:00
settings?: {
noicon?: boolean
noimage?: boolean
nosubtitle?: boolean
noauthor?: boolean
nodate?: boolean
isGroup?: boolean
photoBottom?: boolean
additionalClass?: string
isFeedMode?: boolean
2022-10-19 14:26:49 +00:00
isFloorImportant?: boolean
isWithCover?: boolean
isBigTitle?: boolean
isVertical?: boolean
isShort?: boolean
withBorder?: boolean
isCompact?: boolean
isSingle?: boolean
2022-11-16 21:08:04 +00:00
isBeside?: boolean
2023-07-09 18:34:59 +00:00
withViewed?: boolean
noAuthorLink?: boolean
2022-09-09 11:53:35 +00:00
}
desktopCoverSize: 'XS' | 'S' | 'M' | 'L'
2022-09-09 11:53:35 +00:00
article: Shout
}
const desktopCoverImageWidths: Record<ArticleCardProps['desktopCoverSize'], number> = {
XS: 300,
S: 400,
M: 600,
L: 800,
}
const getTitleAndSubtitle = (
article: Shout,
): {
title: string
subtitle: string
} => {
2022-09-22 09:37:49 +00:00
let title = article.title
let subtitle = article.subtitle
2022-09-09 11:53:35 +00:00
2022-09-22 09:37:49 +00:00
if (!subtitle) {
let tt = article.title?.split('. ') || []
2022-09-09 11:53:35 +00:00
2022-09-22 09:37:49 +00:00
if (tt?.length === 1) {
tt = article.title?.split(/{!|\?|:|;}\s/) || []
}
2022-09-09 11:53:35 +00:00
2022-09-22 09:37:49 +00:00
if (tt && tt.length > 1) {
const sep = article.title?.replace(tt[0], '').split(' ', 1)[0]
2022-11-21 13:02:04 +00:00
title = tt[0] + (sep === '.' || sep === ':' ? '' : sep)
2022-09-22 09:37:49 +00:00
subtitle = capitalize(article.title?.replace(tt[0] + sep, ''), true)
2022-09-09 11:53:35 +00:00
}
}
2022-09-22 09:37:49 +00:00
return { title, subtitle }
}
export const ArticleCard = (props: ArticleCardProps) => {
const { t, lang, formatDate } = useLocalize()
const { user } = useSession()
2023-11-28 13:18:25 +00:00
const mainTopic = props.article.topics[0]
2022-09-22 09:37:49 +00:00
const formattedDate = createMemo<string>(() => {
2023-11-28 13:18:25 +00:00
return formatDate(new Date(props.article.created_at * 1000))
2022-09-22 09:37:49 +00:00
})
const { title, subtitle } = getTitleAndSubtitle(props.article)
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
2023-04-20 14:01:15 +00:00
const { changeSearchParam } = useRouter()
const scrollToComments = (event) => {
event.preventDefault()
openPage(router, 'article', { slug: props.article.slug })
changeSearchParam({
scrollTo: 'comments',
})
2023-04-20 14:01:15 +00:00
}
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
2023-05-16 19:17:47 +00:00
2022-09-09 11:53:35 +00:00
return (
<section
class={clsx(styles.shoutCard, props.settings?.additionalClass, {
2022-10-25 21:45:37 +00:00
[styles.shoutCardShort]: props.settings?.isShort,
[styles.shoutCardPhotoBottom]: props.settings?.noimage && props.settings?.photoBottom,
[styles.shoutCardFeed]: props.settings?.isFeedMode,
[styles.shoutCardFloorImportant]: props.settings?.isFloorImportant,
[styles.shoutCardWithCover]: props.settings?.isWithCover,
[styles.shoutCardBigTitle]: props.settings?.isBigTitle,
[styles.shoutCardVertical]: props.settings?.isVertical,
[styles.shoutCardWithBorder]: props.settings?.withBorder,
[styles.shoutCardCompact]: props.settings?.isCompact,
2022-11-16 21:08:04 +00:00
[styles.shoutCardSingle]: props.settings?.isSingle,
[styles.shoutCardBeside]: props.settings?.isBeside,
[styles.shoutCardNoImage]: !props.article.cover,
})}
2022-09-09 11:53:35 +00:00
>
2023-08-12 14:17:00 +00:00
<Show when={!props.settings?.noimage && !props.settings?.isFeedMode}>
2022-10-25 21:45:37 +00:00
<div class={styles.shoutCardCoverContainer}>
<div
class={clsx(styles.shoutCardCover, {
[styles.loading]: props.article.cover && isCoverImageLoading(),
})}
>
<Show
when={props.article.cover && !isCoverImageLoadError()}
fallback={<CoverImage class={styles.placeholderCoverImage} />}
>
<Image
src={props.article.cover}
alt={title}
width={desktopCoverImageWidths[props.desktopCoverSize]}
onError={() => {
setIsCoverImageLoadError(true)
setIsCoverImageLoading(false)
}}
onLoad={() => setIsCoverImageLoading(false)}
/>
2023-08-12 14:17:00 +00:00
</Show>
2022-09-28 10:34:21 +00:00
</div>
</div>
</Show>
2022-10-25 21:45:37 +00:00
<div class={styles.shoutCardContent}>
2023-06-21 20:32:16 +00:00
<Show
when={
props.article.layout &&
props.article.layout !== 'article' &&
2023-06-21 20:32:16 +00:00
!(props.settings?.noicon || props.settings?.noimage) &&
!props.settings?.isFeedMode
}
>
2022-10-25 21:45:37 +00:00
<div class={styles.shoutCardType}>
<a href={`/expo/${props.article.layout}`}>
<Icon name={props.article.layout} class={styles.icon} />
2023-07-09 18:34:59 +00:00
{/*<Icon name={`${layout}-hover`} class={clsx(styles.icon, styles.iconHover)} />*/}
2022-09-28 10:34:21 +00:00
</a>
2022-09-09 11:53:35 +00:00
</div>
</Show>
<Show when={!props.settings?.isGroup && mainTopic}>
2022-10-19 14:26:49 +00:00
<CardTopic
title={
2023-02-17 09:21:02 +00:00
lang() === 'ru' && mainTopic.title ? mainTopic.title : mainTopic?.slug?.replace('-', ' ')
2022-10-19 14:26:49 +00:00
}
slug={mainTopic.slug}
isFloorImportant={props.settings?.isFloorImportant}
2023-06-21 20:32:16 +00:00
isFeedMode={true}
2023-06-05 21:56:36 +00:00
class={clsx(styles.shoutTopic, { [styles.shoutTopicTop]: props.settings.isShort })}
2022-10-19 14:26:49 +00:00
/>
2022-09-28 10:34:21 +00:00
</Show>
2022-09-09 11:53:35 +00:00
2023-06-21 20:32:16 +00:00
<div
class={clsx(styles.shoutCardTitlesContainer, {
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode,
2023-06-21 20:32:16 +00:00
})}
>
2023-11-04 14:11:58 +00:00
<a href={getPagePath(router, 'article', { slug: props.article.slug })}>
2022-10-25 21:45:37 +00:00
<div class={styles.shoutCardTitle}>
2023-07-09 18:34:59 +00:00
<span class={styles.shoutCardLinkWrapper}>
<span class={styles.shoutCardLinkContainer}>{title}</span>
</span>
2022-09-09 11:53:35 +00:00
</div>
2022-09-28 10:34:21 +00:00
<Show when={!props.settings?.nosubtitle && subtitle}>
2022-10-25 21:45:37 +00:00
<div class={styles.shoutCardSubtitle}>
<span class={styles.shoutCardLinkContainer}>{subtitle}</span>
2022-09-09 11:53:35 +00:00
</div>
2022-09-28 10:34:21 +00:00
</Show>
</a>
</div>
<Show when={!props.settings?.noauthor || !props.settings?.nodate}>
2023-06-21 20:32:16 +00:00
<div
class={clsx(styles.shoutDetails, { [styles.shoutDetailsFeedMode]: props.settings?.isFeedMode })}
>
2022-09-28 10:34:21 +00:00
<Show when={!props.settings?.noauthor}>
2022-10-25 21:45:37 +00:00
<div class={styles.shoutAuthor}>
<For each={props.article.authors}>
2023-06-21 20:32:16 +00:00
{(author) => {
2023-11-20 21:31:52 +00:00
return (
<AuthorLink
size={'XS'}
author={author}
isFloorImportant={props.settings.isFloorImportant || props.settings?.isWithCover}
/>
)
2022-09-28 10:34:21 +00:00
}}
</For>
</div>
</Show>
<Show when={!props.settings?.nodate}>
<time class={styles.shoutDate}>{formattedDate()}</time>
2022-09-28 10:34:21 +00:00
</Show>
2022-09-09 11:53:35 +00:00
</div>
2022-09-28 10:34:21 +00:00
</Show>
<Show when={props.settings?.isFeedMode}>
<Show when={props.article.description}>
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
</Show>
<Show when={!props.settings?.noimage && props.article.cover}>
2023-06-21 20:32:16 +00:00
<div class={styles.shoutCardCoverContainer}>
<Show
when={
props.article.layout &&
props.article.layout !== 'article' &&
!(props.settings?.noicon || props.settings?.noimage)
2023-06-21 20:32:16 +00:00
}
>
<div class={styles.shoutCardType}>
<a href={`/expo/${props.article.layout}`}>
<Icon name={props.article.layout} class={styles.icon} />
2023-07-09 18:34:59 +00:00
{/*<Icon name={`${layout}-hover`} class={clsx(styles.icon, styles.iconHover)} />*/}
2023-06-21 20:32:16 +00:00
</a>
</div>
</Show>
<div class={styles.shoutCardCover}>
<Image src={props.article.cover} alt={title} width={600} loading="lazy" />
2023-06-21 20:32:16 +00:00
</div>
</div>
</Show>
2023-05-16 19:17:47 +00:00
<section
class={styles.shoutCardDetails}
classList={{ [styles.shoutCardDetailsActive]: isActionPopupActive() }}
2023-05-16 19:17:47 +00:00
>
2022-10-25 21:45:37 +00:00
<div class={styles.shoutCardDetailsContent}>
<ShoutRatingControl shout={props.article} class={styles.shoutCardDetailsItem} />
2023-01-25 22:13:01 +00:00
2022-10-25 21:45:37 +00:00
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardComments)}>
2023-04-20 14:01:15 +00:00
<a href="#" onClick={(event) => scrollToComments(event)}>
2023-01-25 22:13:01 +00:00
<Icon name="comment" class={clsx(styles.icon, styles.feedControlIcon)} />
<Icon
name="comment-hover"
class={clsx(styles.icon, styles.iconHover, styles.feedControlIcon)}
/>
<Show
when={props.article.stat?.commented}
fallback={
<span class={clsx(styles.shoutCardLinkContainer, styles.shoutCardDetailsItemLabel)}>
{t('Add comment')}
</span>
}
>
{props.article.stat?.commented}
</Show>
</a>
2022-09-09 11:53:35 +00:00
</div>
2023-07-09 18:34:59 +00:00
<Show when={props.settings?.withViewed}>
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardDetailsViewed)}>
<Icon name="eye" class={clsx(styles.icon, styles.feedControlIcon)} />
{props.article.stat?.viewed}
2023-07-09 18:34:59 +00:00
</div>
</Show>
2023-01-25 22:13:01 +00:00
</div>
2022-09-09 11:53:35 +00:00
2023-01-25 22:13:01 +00:00
<div class={styles.shoutCardDetailsContent}>
<Show when={canEdit()}>
<Popover content={t('Edit')}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutCardDetailsItem} ref={triggerRef}>
<a href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}>
<Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
<Icon
name="pencil-outline-hover"
class={clsx(styles.icon, styles.iconHover, styles.feedControlIcon)}
/>
</a>
</div>
)}
</Popover>
</Show>
2022-09-28 10:34:21 +00:00
<Popover content={t('Add to bookmarks')}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutCardDetailsItem} ref={triggerRef}>
2023-01-25 22:13:01 +00:00
<button>
<Icon name="bookmark" class={clsx(styles.icon, styles.feedControlIcon)} />
<Icon
name="bookmark-hover"
class={clsx(styles.icon, styles.iconHover, styles.feedControlIcon)}
/>
2023-01-25 22:13:01 +00:00
</button>
</div>
)}
</Popover>
<Popover content={t('Share')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutCardDetailsItem} ref={triggerRef}>
<SharePopup
containerCssClass={stylesHeader.control}
title={title}
description={getDescription(props.article.body)}
imageUrl={props.article.cover}
shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })}
isVisible={(value) => setIsActionPopupActive(value)}
trigger={
<button>
<Icon name="share-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
<Icon
name="share-outline-hover"
class={clsx(styles.icon, styles.iconHover, styles.feedControlIcon)}
/>
</button>
}
/>
</div>
)}
</Popover>
2023-01-25 22:13:01 +00:00
<div class={styles.shoutCardDetailsItem}>
2023-02-06 21:35:08 +00:00
<FeedArticlePopup
isOwner={canEdit()}
2023-02-06 21:35:08 +00:00
containerCssClass={stylesHeader.control}
2023-02-07 13:20:07 +00:00
title={title}
description={getDescription(props.article.body)}
imageUrl={props.article.cover}
shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })}
isVisible={(value) => setIsActionPopupActive(value)}
2023-02-06 21:35:08 +00:00
trigger={
<button>
<Icon name="ellipsis" class={clsx(styles.icon, styles.feedControlIcon)} />
<Icon
name="ellipsis"
class={clsx(styles.icon, styles.iconHover, styles.feedControlIcon)}
/>
2023-02-06 21:35:08 +00:00
</button>
}
/>
2023-01-25 22:13:01 +00:00
</div>
</div>
2022-09-28 10:34:21 +00:00
</section>
</Show>
</div>
2022-09-09 11:53:35 +00:00
</section>
)
}