import type { Author, Shout } from '../../graphql/types.gen' import { getPagePath } from '@nanostores/router' import { createPopper } from '@popperjs/core' import { clsx } from 'clsx' import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js' import { useLocalize } from '../../context/localize' import { useReactions } from '../../context/reactions' import { useSession } from '../../context/session' import { MediaItem } from '../../pages/types' import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router' import { getImageUrl } from '../../utils/getImageUrl' import { getDescription } from '../../utils/meta' import { Icon } from '../_shared/Icon' import { Image } from '../_shared/Image' import { Lightbox } from '../_shared/Lightbox' import { Popover } from '../_shared/Popover' import { ImageSwiper } from '../_shared/SolidSwiper' import { VideoPlayer } from '../_shared/VideoPlayer' import { AuthorBadge } from '../Author/AuthorBadge' import { CardTopic } from '../Feed/CardTopic' import { FeedArticlePopup } from '../Feed/FeedArticlePopup' import { TableOfContents } from '../TableOfContents' import { AudioHeader } from './AudioHeader' import { AudioPlayer } from './AudioPlayer' import { CommentsTree } from './CommentsTree' import { getShareUrl, SharePopup } from './SharePopup' import { ShoutRatingControl } from './ShoutRatingControl' import styles from './Article.module.scss' import stylesHeader from '../Nav/Header/Header.module.scss' type Props = { article: Shout scrollToComments?: boolean } export type ArticlePageSearchParams = { scrollTo: 'comments' commentId: string } const scrollTo = (el: HTMLElement) => { const { top } = el.getBoundingClientRect() window.scrollTo({ top: top + window.scrollY - DEFAULT_HEADER_OFFSET, left: 0, behavior: 'smooth', }) } export const FullArticle = (props: Props) => { const [selectedImage, setSelectedImage] = createSignal('') const { t, formatDate } = useLocalize() const { user, isAuthenticated, actions: { requireAuthentication }, } = useSession() const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt))) const mainTopic = createMemo( () => props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) || props.article.topics[0], ) const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug) const handleBookmarkButtonClick = (ev) => { requireAuthentication(() => { // TODO: implement bookmark clicked ev.preventDefault() }, 'bookmark') } 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(() => { try { return JSON.parse(props.article.media) } catch { return [] } }) const commentsRef: { current: HTMLDivElement } = { current: null } const scrollToComments = () => { scrollTo(commentsRef.current) } const { searchParams, changeSearchParam } = useRouter() createEffect(() => { if (props.scrollToComments) { scrollToComments() } }) createEffect(() => { if (searchParams()?.scrollTo === 'comments' && commentsRef.current) { scrollToComments() changeSearchParam({ scrollTo: null, }) } }) createEffect(() => { if (searchParams().commentId && isReactionsLoaded()) { const commentElement = document.querySelector( `[id='comment_${searchParams().commentId}']`, ) changeSearchParam({ commentId: null }) if (commentElement) { scrollTo(commentElement) } } }) const { actions: { loadReactionsBy }, } = useReactions() onMount(async () => { await loadReactionsBy({ by: { shout: props.article.slug }, }) setIsReactionsLoaded(true) }) onMount(() => { document.title = props.article.title }) const clickHandlers = [] const documentClickHandlers = [] createEffect(() => { if (!body()) { return } const tooltipElements: NodeListOf = document.querySelectorAll( '[data-toggle="tooltip"], footnote', ) if (!tooltipElements) { return } tooltipElements.forEach((element) => { const tooltip = document.createElement('div') tooltip.classList.add(styles.tooltip) const tooltipContent = document.createElement('div') tooltipContent.classList.add(styles.tooltipContent) tooltipContent.innerHTML = element.dataset.originalTitle || element.dataset.value tooltip.appendChild(tooltipContent) document.body.appendChild(tooltip) if (element.hasAttribute('href')) { element.setAttribute('href', 'javascript: void(0)') } const popperInstance = createPopper(element, tooltip, { placement: 'top', modifiers: [ { name: 'eventListeners', options: { scroll: false }, }, { name: 'offset', options: { offset: [0, 8], }, }, { name: 'flip', options: { fallbackPlacements: ['top'] }, }, ], }) tooltip.style.visibility = 'hidden' let isTooltipVisible = false const handleClick = () => { if (isTooltipVisible) { tooltip.style.visibility = 'hidden' isTooltipVisible = false } else { tooltip.style.visibility = 'visible' isTooltipVisible = true } popperInstance.update() } const handleDocumentClick = (e) => { if (isTooltipVisible && e.target !== element && e.target !== tooltip) { tooltip.style.visibility = 'hidden' isTooltipVisible = false } } element.addEventListener('click', handleClick) document.addEventListener('click', handleDocumentClick) clickHandlers.push({ element, handler: handleClick }) documentClickHandlers.push(handleDocumentClick) }) }) onCleanup(() => { clickHandlers.forEach(({ element, handler }) => { element.removeEventListener('click', handler) }) documentClickHandlers.forEach((handler) => { document.removeEventListener('click', handler) }) }) const openLightbox = (image) => { setSelectedImage(image) } const handleLightboxClose = () => { setSelectedImage() } const handleArticleBodyClick = (event) => { if (event.target.tagName === 'IMG') { const src = event.target.src openLightbox(getImageUrl(src)) } } return ( <>
{/*TODO: Check styles.shoutTopic*/}

{props.article.title}

{props.article.subtitle}

{(a: Author, index) => ( <> 0}>, {a.name} )}
{props.article.title}
0}>
{(m: MediaItem) => (
)}
{(triggerRef: (el) => void) => (
{t('Add comment')}} > {props.article.stat?.commented}
)}
{t('viewsWithCount', { count: props.article.stat?.viewed })}
{formattedDate()}
{(triggerRef: (el) => void) => (
)}
{(triggerRef: (el) => void) => (
} />
)} {(triggerRef: (el) => void) => ( )} } />
{(topic) => ( )}
1}>

{t('Authors')}

{(author) => (
)}
(commentsRef.current = el)}>
) }