webapp/src/components/Article/FullArticle.tsx

375 lines
13 KiB
TypeScript
Raw Normal View History

import { createEffect, For, createMemo, onMount, Show, createSignal } from 'solid-js'
import { Title } from '@solidjs/meta'
import { clsx } from 'clsx'
import { getPagePath } from '@nanostores/router'
import MD from './MD'
import type { Author, Shout } from '../../graphql/types.gen'
import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { MediaItem } from '../../pages/types'
import { router, useRouter } from '../../stores/router'
import { formatDate } from '../../utils'
import { getDescription } from '../../utils/meta'
import { imageProxy } from '../../utils/imageProxy'
import { isDesktop } from '../../utils/media-query'
2023-05-01 18:32:32 +00:00
import { AuthorCard } from '../Author/AuthorCard'
import { TableOfContents } from '../TableOfContents'
import { AudioPlayer } from './AudioPlayer'
2023-02-10 01:19:20 +00:00
import { SharePopup } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl'
2022-11-26 16:51:08 +00:00
import { CommentsTree } from './CommentsTree'
2023-03-08 16:35:13 +00:00
import stylesHeader from '../Nav/Header.module.scss'
import { AudioHeader } from './AudioHeader'
import { Popover } from '../_shared/Popover'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { Icon } from '../_shared/Icon'
2023-07-17 17:14:34 +00:00
import { SolidSwiper } from '../_shared/SolidSwiper'
import styles from './Article.module.scss'
2023-08-12 14:17:00 +00:00
import { CardTopic } from '../Feed/CardTopic'
2023-08-17 11:11:58 +00:00
import { createPopper } from '@popperjs/core'
interface Props {
2022-09-09 11:53:35 +00:00
article: Shout
2023-04-17 10:31:20 +00:00
scrollToComments?: boolean
2022-09-09 11:53:35 +00:00
}
export const FullArticle = (props: Props) => {
2023-02-17 09:21:02 +00:00
const { t } = useLocalize()
const {
user,
isAuthenticated,
actions: { requireAuthentication }
} = useSession()
2023-02-28 17:13:14 +00:00
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
2022-09-09 11:53:35 +00:00
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
2022-12-07 18:38:05 +00:00
const mainTopic = createMemo(
() =>
props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) ||
props.article.topics[0]
)
2023-03-29 08:51:27 +00:00
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
2023-05-01 18:32:32 +00:00
const handleBookmarkButtonClick = (ev) => {
requireAuthentication(() => {
// TODO: implement bookmark clicked
ev.preventDefault()
}, 'bookmark')
2022-11-27 11:00:44 +00:00
}
const body = createMemo(() => {
if (props.article.layout === 'literature') {
try {
const media = JSON.parse(props.article.media)
if (media.length > 0) {
return media[0].body
}
} catch (error) {
console.error(error)
}
}
return props.article.body
})
const media = createMemo(() => {
return JSON.parse(props.article.media || '[]')
})
2022-11-27 11:00:44 +00:00
2023-04-17 10:31:20 +00:00
const commentsRef: { current: HTMLDivElement } = { current: null }
const scrollToComments = () => {
window.scrollTo({
top: commentsRef.current.offsetTop - 96,
left: 0,
behavior: 'smooth'
})
}
2023-05-01 18:32:32 +00:00
const { searchParams, changeSearchParam } = useRouter()
2023-04-17 10:31:20 +00:00
createEffect(() => {
if (props.scrollToComments) {
scrollToComments()
}
})
2023-05-01 18:32:32 +00:00
2023-04-20 14:01:15 +00:00
createEffect(() => {
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
scrollToComments()
changeSearchParam('scrollTo', null)
}
})
2023-04-17 10:31:20 +00:00
createEffect(() => {
if (searchParams().commentId && isReactionsLoaded()) {
const commentElement = document.querySelector(`[id='comment_${searchParams().commentId}']`)
if (commentElement) {
commentElement.scrollIntoView({ behavior: 'smooth' })
}
}
})
2023-02-17 09:21:02 +00:00
const {
actions: { loadReactionsBy }
2023-02-17 09:21:02 +00:00
} = useReactions()
onMount(async () => {
await loadReactionsBy({
by: { shout: props.article.slug }
})
setIsReactionsLoaded(true)
})
2023-08-17 11:11:58 +00:00
onMount(() => {
const tooltipElements: NodeListOf<HTMLLinkElement> =
document.querySelectorAll('[data-toggle="tooltip"]')
if (!tooltipElements) return
tooltipElements.forEach((element) => {
const tooltip = document.createElement('div')
tooltip.classList.add(styles.tooltip)
tooltip.textContent = element.dataset.originalTitle
document.body.appendChild(tooltip)
createPopper(element, tooltip, { placement: 'top' })
tooltip.style.visibility = 'hidden'
element.addEventListener('mouseenter', () => {
tooltip.style.visibility = 'visible'
})
element.addEventListener('mouseleave', () => {
tooltip.style.visibility = 'hidden'
})
})
})
2022-09-09 11:53:35 +00:00
return (
2023-02-09 22:54:53 +00:00
<>
2023-02-17 09:21:02 +00:00
<Title>{props.article.title}</Title>
2023-03-08 16:35:13 +00:00
<div class="wide-container">
<div class="row position-relative">
2023-03-10 17:42:48 +00:00
<article class="col-md-16 col-lg-14 col-xl-12 offset-md-5">
{/*TODO: Check styles.shoutTopic*/}
2023-07-17 17:14:34 +00:00
<Show when={props.article.layout !== 'audio'}>
<div class={styles.shoutHeader}>
<Show when={mainTopic()}>
2023-08-12 14:17:00 +00:00
<CardTopic title={mainTopic().title} slug={props.article.mainTopic} />
2023-07-17 17:14:34 +00:00
</Show>
<h1>{props.article.title}</h1>
<Show when={props.article.subtitle}>
2023-07-31 09:57:50 +00:00
<h4>{props.article.subtitle}</h4>
2023-07-17 17:14:34 +00:00
</Show>
<div class={styles.shoutAuthor}>
<For each={props.article.authors}>
{(a: Author, index) => (
<>
<Show when={index() > 0}>, </Show>
<a href={getPagePath(router, 'author', { slug: a.slug })}>{a.name}</a>
</>
)}
</For>
</div>
<Show
when={
props.article.cover &&
props.article.layout !== 'video' &&
props.article.layout !== 'image'
}
>
2023-07-17 17:14:34 +00:00
<div
class={styles.shoutCover}
style={{ 'background-image': `url('${imageProxy(props.article.cover)}')` }}
/>
</Show>
</div>
</Show>
<Show when={props.article.layout === 'audio'}>
<AudioHeader
title={props.article.title}
cover={props.article.cover}
artistData={media()?.[0]}
topic={mainTopic()}
/>
<Show when={media().length > 0}>
<div class="media-items">
<AudioPlayer media={media()} articleSlug={props.article.slug} body={body()} />
2023-05-08 17:21:06 +00:00
</div>
2023-07-17 17:14:34 +00:00
</Show>
</Show>
<Show when={media() && props.article.layout === 'video'}>
<div class="media-items">
<For each={media() || []}>
{(m: MediaItem) => (
<div class={styles.shoutMediaBody}>
<VideoPlayer
articleView={true}
videoUrl={m.url}
title={m.title}
description={m.body}
/>
<Show when={m?.body}>
<MD body={m.body} />
</Show>
</div>
)}
</For>
</div>
</Show>
<Show when={body()}>
<div id="shoutBody" class={styles.shoutBody}>
<Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}>
<MD body={body()} />
</Show>
</div>
</Show>
2023-03-10 17:42:48 +00:00
</article>
<Show when={isDesktop() && body()}>
<TableOfContents variant="article" parentSelector="#shoutBody" body={body()} />
</Show>
2023-03-10 17:42:48 +00:00
</div>
2023-02-09 22:54:53 +00:00
</div>
2022-11-26 21:27:54 +00:00
2023-07-17 22:24:37 +00:00
<Show when={props.article.layout === 'image'}>
<div class="floor floor--important">
<div class="wide-container">
<div class="row">
<div class="col-md-20 offset-md-2">
<SolidSwiper images={media()} />
</div>
</div>
</div>
</div>
</Show>
<div class="wide-container">
2023-03-10 17:42:48 +00:00
<div class="row">
<div class="col-md-16 offset-md-5">
<div class={styles.shoutStats}>
<div class={styles.shoutStatsItem}>
<ShoutRatingControl shout={props.article} class={styles.ratingControl} />
</div>
2023-02-09 22:54:53 +00:00
<Popover content={t('Comment')}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef} onClick={scrollToComments}>
<Icon name="comment" class={styles.icon} />
2023-07-09 18:34:59 +00:00
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
{props.article.stat?.commented ?? ''}
</div>
)}
</Popover>
2023-07-09 18:34:59 +00:00
<Show when={props.article.stat?.viewed}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
<Icon name="eye" class={styles.icon} />
<Icon name="eye" class={clsx(styles.icon, styles.iconHover)} />
{props.article.stat?.viewed}
</div>
</Show>
<Popover content={t('Share')}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}>
<SharePopup
title={props.article.title}
description={getDescription(props.article.body)}
imageUrl={props.article.cover}
containerCssClass={stylesHeader.control}
trigger={
<div class={styles.shoutStatsItemInner}>
<Icon name="share-outline" class={styles.icon} />
2023-07-09 18:34:59 +00:00
<Icon name="share-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
</div>
}
/>
</div>
)}
</Popover>
<Popover content={t('Add to bookmarks')}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef} onClick={handleBookmarkButtonClick}>
2023-03-29 20:18:25 +00:00
<div class={styles.shoutStatsItemInner}>
<Icon name="bookmark" class={styles.icon} />
2023-07-09 18:34:59 +00:00
<Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} />
2023-03-29 20:18:25 +00:00
</div>
</div>
)}
</Popover>
2023-03-10 17:42:48 +00:00
<Show when={canEdit()}>
<Popover content={t('Edit')}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}>
<a
href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}
class={styles.shoutStatsItemInner}
>
<Icon name="pencil-outline" class={styles.icon} />
2023-07-09 18:34:59 +00:00
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
</a>
</div>
)}
</Popover>
2023-03-10 17:42:48 +00:00
</Show>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalData)}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
{formattedDate()}
2023-02-09 22:54:53 +00:00
</div>
2023-03-10 17:42:48 +00:00
</div>
</div>
<div class={styles.help}>
<Show when={isAuthenticated() && !canEdit()}>
2023-03-10 17:42:48 +00:00
<button class="button">{t('Cooperate')}</button>
</Show>
<Show when={canEdit()}>
<button class="button button--light">{t('Invite to collab')}</button>
</Show>
</div>
<Show when={props.article.topics.length}>
<div class={styles.topicsList}>
<For each={props.article.topics}>
{(topic) => (
<div class={styles.shoutTopic}>
<a href={getPagePath(router, 'topic', { slug: topic.slug })}>{topic.title}</a>
</div>
)}
</For>
</div>
</Show>
2023-03-10 17:42:48 +00:00
<div class={styles.shoutAuthorsList}>
<Show when={props.article.authors.length > 1}>
<h4>{t('Authors')}</h4>
</Show>
<For each={props.article.authors}>
{(a) => (
<div class="col-xl-12">
2023-04-11 14:07:57 +00:00
<AuthorCard author={a} hasLink={true} liteButtons={true} />
2023-03-10 17:42:48 +00:00
</div>
)}
</For>
</div>
2023-04-17 10:31:20 +00:00
<div id="comments" ref={(el) => (commentsRef.current = el)}>
2023-03-10 17:42:48 +00:00
<Show when={isReactionsLoaded()}>
<CommentsTree
shoutId={props.article.id}
shoutSlug={props.article.slug}
commentAuthors={props.article.authors}
/>
</Show>
</div>
2023-02-09 22:54:53 +00:00
</div>
2022-09-09 11:53:35 +00:00
</div>
</div>
2023-02-09 22:54:53 +00:00
</>
2022-09-09 11:53:35 +00:00
)
}