2023-12-14 00:04:07 +00:00
|
|
|
import type { Author, Shout, Topic } from '../../graphql/schema/core.gen'
|
2023-11-14 10:45:44 +00:00
|
|
|
|
2023-07-31 21:43:41 +00:00
|
|
|
import { getPagePath } from '@nanostores/router'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { createPopper } from '@popperjs/core'
|
2023-12-13 10:39:31 +00:00
|
|
|
import { Link, Meta } from '@solidjs/meta'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { clsx } from 'clsx'
|
2024-01-29 13:19:04 +00:00
|
|
|
import { install } from 'ga-gtag'
|
2024-02-04 11:25:21 +00:00
|
|
|
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
2023-11-18 14:10:02 +00:00
|
|
|
import { isServer } from 'solid-js/web'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
2023-07-31 21:43:41 +00:00
|
|
|
import { useLocalize } from '../../context/localize'
|
|
|
|
import { useReactions } from '../../context/reactions'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { useSession } from '../../context/session'
|
2023-07-31 21:43:41 +00:00
|
|
|
import { MediaItem } from '../../pages/types'
|
2023-10-18 10:56:41 +00:00
|
|
|
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
|
2024-01-13 14:28:22 +00:00
|
|
|
import { showModal } from '../../stores/ui'
|
2023-12-14 00:04:07 +00:00
|
|
|
import { capitalize } from '../../utils/capitalize'
|
2024-01-06 23:52:24 +00:00
|
|
|
import { getImageUrl, getOpenGraphImageUrl } from '../../utils/getImageUrl'
|
2023-12-13 10:39:31 +00:00
|
|
|
import { getDescription, getKeywords } from '../../utils/meta'
|
2024-02-04 17:40:15 +00:00
|
|
|
import { isCyrillic } from '../../utils/translate'
|
2024-02-04 11:25:21 +00:00
|
|
|
import { AuthorBadge } from '../Author/AuthorBadge'
|
|
|
|
import { CardTopic } from '../Feed/CardTopic'
|
|
|
|
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
|
|
|
|
import { Modal } from '../Nav/Modal'
|
|
|
|
import { TableOfContents } from '../TableOfContents'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { Icon } from '../_shared/Icon'
|
|
|
|
import { Image } from '../_shared/Image'
|
2024-01-25 09:57:57 +00:00
|
|
|
import { InviteMembers } from '../_shared/InviteMembers'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { Lightbox } from '../_shared/Lightbox'
|
|
|
|
import { Popover } from '../_shared/Popover'
|
2024-01-05 19:31:28 +00:00
|
|
|
import { ShareModal } from '../_shared/ShareModal'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { ImageSwiper } from '../_shared/SolidSwiper'
|
|
|
|
import { VideoPlayer } from '../_shared/VideoPlayer'
|
|
|
|
|
|
|
|
import { AudioHeader } from './AudioHeader'
|
2023-07-14 13:06:21 +00:00
|
|
|
import { AudioPlayer } from './AudioPlayer'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { CommentsTree } from './CommentsTree'
|
2024-02-04 11:25:21 +00:00
|
|
|
import { SharePopup, getShareUrl } from './SharePopup'
|
2023-03-03 18:26:26 +00:00
|
|
|
import { ShoutRatingControl } from './ShoutRatingControl'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
|
|
|
import stylesHeader from '../Nav/Header/Header.module.scss'
|
2024-02-04 11:25:21 +00:00
|
|
|
import styles from './Article.module.scss'
|
2023-07-31 21:43:41 +00:00
|
|
|
|
2023-10-16 17:24:33 +00:00
|
|
|
type 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
|
|
|
}
|
|
|
|
|
2024-01-22 10:45:21 +00:00
|
|
|
type IframeSize = {
|
|
|
|
width: number
|
|
|
|
height: number
|
|
|
|
}
|
|
|
|
|
2023-10-16 17:24:33 +00:00
|
|
|
export type ArticlePageSearchParams = {
|
|
|
|
scrollTo: 'comments'
|
|
|
|
commentId: string
|
|
|
|
}
|
|
|
|
|
|
|
|
const scrollTo = (el: HTMLElement) => {
|
2023-10-16 19:50:22 +00:00
|
|
|
const { top } = el.getBoundingClientRect()
|
|
|
|
|
2023-10-16 17:24:33 +00:00
|
|
|
window.scrollTo({
|
2023-10-18 10:56:41 +00:00
|
|
|
top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
|
2023-10-16 17:24:33 +00:00
|
|
|
left: 0,
|
2023-11-14 15:10:00 +00:00
|
|
|
behavior: 'smooth',
|
2023-10-16 17:24:33 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-11-18 14:10:02 +00:00
|
|
|
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
|
|
|
|
|
2023-07-31 21:43:41 +00:00
|
|
|
export const FullArticle = (props: Props) => {
|
2024-02-04 17:40:15 +00:00
|
|
|
const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>()
|
|
|
|
const { loadReactionsBy } = useReactions()
|
2023-11-13 08:05:05 +00:00
|
|
|
const [selectedImage, setSelectedImage] = createSignal('')
|
2024-01-05 19:31:28 +00:00
|
|
|
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
|
|
|
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
2023-12-08 11:49:37 +00:00
|
|
|
const { t, formatDate, lang } = useLocalize()
|
2024-02-16 10:21:25 +00:00
|
|
|
const { author, session, isAuthenticated, requireAuthentication } = useSession()
|
2023-08-28 11:48:54 +00:00
|
|
|
|
2024-02-02 17:29:53 +00:00
|
|
|
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
|
2024-02-16 10:21:25 +00:00
|
|
|
const canEdit = createMemo(
|
|
|
|
() =>
|
|
|
|
props.article.authors?.some((a) => Boolean(a) && a?.slug === author()?.slug) ||
|
|
|
|
props.article?.created_by.slug === author()?.slug ||
|
|
|
|
session()?.user?.roles.includes('editor'),
|
|
|
|
)
|
2022-09-09 11:53:35 +00:00
|
|
|
|
2023-12-08 11:49:37 +00:00
|
|
|
const mainTopic = createMemo(() => {
|
2024-02-05 15:04:23 +00:00
|
|
|
const mainTopicSlug = props.article.topics.length > 0 ? props.article.main_topic : null
|
|
|
|
const mt = props.article.topics.find((tpc: Topic) => tpc.slug === mainTopicSlug)
|
2023-12-09 18:35:08 +00:00
|
|
|
if (mt) {
|
2023-12-28 00:52:54 +00:00
|
|
|
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title
|
2023-12-09 18:35:08 +00:00
|
|
|
return mt
|
|
|
|
}
|
2024-02-04 09:03:15 +00:00
|
|
|
return props.article.topics[0]
|
2023-12-08 11:49:37 +00:00
|
|
|
})
|
2022-12-07 18:38:05 +00:00
|
|
|
|
2023-05-01 18:32:32 +00:00
|
|
|
const handleBookmarkButtonClick = (ev) => {
|
2023-06-14 17:19:30 +00:00
|
|
|
requireAuthentication(() => {
|
|
|
|
// TODO: implement bookmark clicked
|
|
|
|
ev.preventDefault()
|
|
|
|
}, 'bookmark')
|
2022-11-27 11:00:44 +00:00
|
|
|
}
|
|
|
|
|
2023-07-17 19:07:37 +00:00
|
|
|
const body = createMemo(() => {
|
|
|
|
if (props.article.layout === 'literature') {
|
|
|
|
try {
|
2023-12-20 07:45:29 +00:00
|
|
|
if (props.article?.media) {
|
|
|
|
const media = JSON.parse(props.article.media)
|
|
|
|
if (media.length > 0) {
|
|
|
|
return media[0].body
|
|
|
|
}
|
2023-07-17 19:07:37 +00:00
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return props.article.body
|
|
|
|
})
|
2023-11-14 10:45:44 +00:00
|
|
|
|
2023-11-18 14:10:02 +00:00
|
|
|
const imageUrls = createMemo(() => {
|
|
|
|
if (!body()) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isServer) {
|
|
|
|
const result: string[] = []
|
|
|
|
let match: RegExpMatchArray
|
|
|
|
|
|
|
|
while ((match = imgSrcRegExp.exec(body())) !== null) {
|
|
|
|
result.push(match[1])
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
const imageElements = document.querySelectorAll<HTMLImageElement>('#shoutBody img')
|
|
|
|
// eslint-disable-next-line unicorn/prefer-spread
|
|
|
|
return Array.from(imageElements).map((img) => img.src)
|
|
|
|
})
|
|
|
|
|
2023-11-14 10:45:44 +00:00
|
|
|
const media = createMemo<MediaItem[]>(() => {
|
|
|
|
try {
|
|
|
|
return JSON.parse(props.article.media)
|
|
|
|
} catch {
|
|
|
|
return []
|
|
|
|
}
|
2023-07-02 05:08:42 +00:00
|
|
|
})
|
2022-11-27 11:00:44 +00:00
|
|
|
|
2023-10-16 19:50:22 +00:00
|
|
|
const commentsRef: {
|
|
|
|
current: HTMLDivElement
|
|
|
|
} = { current: null }
|
2023-10-16 17:24:33 +00:00
|
|
|
|
2023-04-17 10:31:20 +00:00
|
|
|
const scrollToComments = () => {
|
2023-10-16 17:24:33 +00:00
|
|
|
scrollTo(commentsRef.current)
|
2023-04-17 10:31:20 +00:00
|
|
|
}
|
2023-05-26 11:30:27 +00:00
|
|
|
|
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()
|
2023-12-20 08:07:57 +00:00
|
|
|
changeSearchParams({
|
2023-11-14 15:10:00 +00:00
|
|
|
scrollTo: null,
|
2023-09-29 12:48:58 +00:00
|
|
|
})
|
2023-04-20 14:01:15 +00:00
|
|
|
}
|
|
|
|
})
|
2023-04-17 10:31:20 +00:00
|
|
|
|
2023-05-26 11:30:27 +00:00
|
|
|
createEffect(() => {
|
|
|
|
if (searchParams().commentId && isReactionsLoaded()) {
|
2023-10-16 17:24:33 +00:00
|
|
|
const commentElement = document.querySelector<HTMLElement>(
|
2023-11-14 15:10:00 +00:00
|
|
|
`[id='comment_${searchParams().commentId}']`,
|
2023-10-16 17:24:33 +00:00
|
|
|
)
|
2023-10-16 19:50:22 +00:00
|
|
|
|
2023-12-20 08:07:57 +00:00
|
|
|
changeSearchParams({ commentId: null })
|
2023-10-16 19:50:22 +00:00
|
|
|
|
2023-05-26 11:30:27 +00:00
|
|
|
if (commentElement) {
|
2023-10-16 17:24:33 +00:00
|
|
|
scrollTo(commentElement)
|
2023-05-26 11:30:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-08-21 11:15:32 +00:00
|
|
|
const clickHandlers = []
|
|
|
|
const documentClickHandlers = []
|
|
|
|
|
2023-08-28 11:48:54 +00:00
|
|
|
createEffect(() => {
|
|
|
|
if (!body()) {
|
|
|
|
return
|
|
|
|
}
|
2023-08-21 11:15:32 +00:00
|
|
|
|
2023-08-28 11:48:54 +00:00
|
|
|
const tooltipElements: NodeListOf<HTMLElement> = document.querySelectorAll(
|
2023-11-14 15:10:00 +00:00
|
|
|
'[data-toggle="tooltip"], footnote',
|
2023-08-28 11:48:54 +00:00
|
|
|
)
|
2023-09-07 18:45:22 +00:00
|
|
|
if (!tooltipElements) {
|
|
|
|
return
|
|
|
|
}
|
2023-08-17 11:11:58 +00:00
|
|
|
tooltipElements.forEach((element) => {
|
|
|
|
const tooltip = document.createElement('div')
|
|
|
|
tooltip.classList.add(styles.tooltip)
|
2023-09-05 05:49:19 +00:00
|
|
|
const tooltipContent = document.createElement('div')
|
|
|
|
tooltipContent.classList.add(styles.tooltipContent)
|
|
|
|
tooltipContent.innerHTML = element.dataset.originalTitle || element.dataset.value
|
|
|
|
|
2024-01-23 16:44:58 +00:00
|
|
|
tooltip.append(tooltipContent)
|
2023-09-05 05:49:19 +00:00
|
|
|
|
2024-01-23 16:44:58 +00:00
|
|
|
document.body.append(tooltip)
|
2023-09-05 07:59:36 +00:00
|
|
|
|
|
|
|
if (element.hasAttribute('href')) {
|
2023-11-13 08:05:05 +00:00
|
|
|
element.setAttribute('href', 'javascript: void(0)')
|
2023-08-28 11:48:54 +00:00
|
|
|
}
|
2023-09-07 18:45:22 +00:00
|
|
|
|
|
|
|
const popperInstance = createPopper(element, tooltip, {
|
2023-08-21 11:15:32 +00:00
|
|
|
placement: 'top',
|
|
|
|
modifiers: [
|
2023-09-07 07:25:02 +00:00
|
|
|
{
|
|
|
|
name: 'eventListeners',
|
2023-11-14 15:10:00 +00:00
|
|
|
options: { scroll: false },
|
2023-09-07 07:25:02 +00:00
|
|
|
},
|
2023-08-21 11:15:32 +00:00
|
|
|
{
|
|
|
|
name: 'offset',
|
|
|
|
options: {
|
2023-11-14 15:10:00 +00:00
|
|
|
offset: [0, 8],
|
|
|
|
},
|
2023-09-07 18:45:22 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'flip',
|
2023-11-14 15:10:00 +00:00
|
|
|
options: { fallbackPlacements: ['top'] },
|
|
|
|
},
|
|
|
|
],
|
2023-08-17 11:11:58 +00:00
|
|
|
})
|
2023-08-21 11:15:32 +00:00
|
|
|
|
|
|
|
tooltip.style.visibility = 'hidden'
|
|
|
|
let isTooltipVisible = false
|
|
|
|
const handleClick = () => {
|
|
|
|
if (isTooltipVisible) {
|
|
|
|
tooltip.style.visibility = 'hidden'
|
|
|
|
isTooltipVisible = false
|
|
|
|
} else {
|
|
|
|
tooltip.style.visibility = 'visible'
|
|
|
|
isTooltipVisible = true
|
|
|
|
}
|
2023-09-07 18:45:22 +00:00
|
|
|
|
|
|
|
popperInstance.update()
|
2023-08-21 11:15:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2023-08-17 11:11:58 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-11-13 08:05:05 +00:00
|
|
|
const openLightbox = (image) => {
|
|
|
|
setSelectedImage(image)
|
|
|
|
}
|
|
|
|
const handleLightboxClose = () => {
|
|
|
|
setSelectedImage()
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleArticleBodyClick = (event) => {
|
2024-02-04 09:03:15 +00:00
|
|
|
if (event.target.tagName === 'IMG' && !event.target.dataset.disableLightbox) {
|
2023-11-13 08:05:05 +00:00
|
|
|
const src = event.target.src
|
|
|
|
openLightbox(getImageUrl(src))
|
|
|
|
}
|
|
|
|
}
|
2023-11-06 19:15:13 +00:00
|
|
|
|
2024-01-22 10:45:21 +00:00
|
|
|
// Check iframes size
|
|
|
|
const articleContainer: { current: HTMLElement } = { current: null }
|
|
|
|
const updateIframeSizes = () => {
|
2024-02-04 17:40:15 +00:00
|
|
|
if (!(articleContainer?.current && props.article.body)) return
|
2024-01-22 10:45:21 +00:00
|
|
|
const iframes = articleContainer?.current?.querySelectorAll('iframe')
|
|
|
|
if (!iframes) return
|
|
|
|
const containerWidth = articleContainer.current?.offsetWidth
|
|
|
|
iframes.forEach((iframe) => {
|
|
|
|
const style = window.getComputedStyle(iframe)
|
|
|
|
const originalWidth = iframe.getAttribute('width') || style.width.replace('px', '')
|
|
|
|
const originalHeight = iframe.getAttribute('height') || style.height.replace('px', '')
|
|
|
|
|
2024-01-23 16:32:57 +00:00
|
|
|
const width: IframeSize['width'] = Number(originalWidth)
|
|
|
|
const height: IframeSize['height'] = Number(originalHeight)
|
2024-01-22 10:45:21 +00:00
|
|
|
|
|
|
|
if (containerWidth < width) {
|
|
|
|
const aspectRatio = width / height
|
|
|
|
iframe.style.width = `${containerWidth}px`
|
|
|
|
iframe.style.height = `${Math.round(containerWidth / aspectRatio) + 40}px`
|
2024-02-01 20:46:54 +00:00
|
|
|
} else {
|
|
|
|
iframe.style.height = `${containerWidth}px`
|
2024-01-22 10:45:21 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
createEffect(
|
|
|
|
on(
|
|
|
|
() => props.article,
|
|
|
|
() => {
|
|
|
|
updateIframeSizes()
|
|
|
|
},
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
onMount(async () => {
|
2024-01-29 12:49:37 +00:00
|
|
|
install('G-LQ4B87H8C2')
|
|
|
|
await loadReactionsBy({ by: { shout: props.article.slug } })
|
2024-01-22 10:45:21 +00:00
|
|
|
setIsReactionsLoaded(true)
|
|
|
|
document.title = props.article.title
|
|
|
|
window?.addEventListener('resize', updateIframeSizes)
|
2024-01-06 23:52:24 +00:00
|
|
|
|
2024-01-22 10:45:21 +00:00
|
|
|
onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
|
|
|
|
})
|
|
|
|
|
|
|
|
const cover = props.article.cover ?? 'production/image/logo_image.png'
|
2024-01-06 23:52:24 +00:00
|
|
|
const ogImage = getOpenGraphImageUrl(cover, {
|
|
|
|
title: props.article.title,
|
2024-02-05 08:59:21 +00:00
|
|
|
topic: mainTopic()?.title || '',
|
|
|
|
author: props.article?.authors[0]?.name || '',
|
2024-01-06 23:52:24 +00:00
|
|
|
width: 1200,
|
|
|
|
})
|
|
|
|
|
2023-12-13 10:39:31 +00:00
|
|
|
const description = getDescription(props.article.description || body())
|
|
|
|
const ogTitle = props.article.title
|
|
|
|
const keywords = getKeywords(props.article)
|
2024-01-05 19:31:28 +00:00
|
|
|
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
|
2023-12-19 09:34:24 +00:00
|
|
|
const getAuthorName = (a: Author) => {
|
2023-12-20 16:54:20 +00:00
|
|
|
return lang() === 'en' && isCyrillic(a.name) ? capitalize(a.slug.replace(/-/, ' ')) : a.name
|
2023-12-19 09:34:24 +00:00
|
|
|
}
|
2022-09-09 11:53:35 +00:00
|
|
|
return (
|
2023-02-09 22:54:53 +00:00
|
|
|
<>
|
2023-12-13 10:39:31 +00:00
|
|
|
<Meta name="descprition" content={description} />
|
|
|
|
<Meta name="keywords" content={keywords} />
|
|
|
|
<Meta name="og:type" content="article" />
|
|
|
|
<Meta name="og:title" content={ogTitle} />
|
|
|
|
<Meta name="og:image" content={ogImage} />
|
|
|
|
<Meta name="og:description" content={description} />
|
|
|
|
<Meta name="twitter:card" content="summary_large_image" />
|
|
|
|
<Meta name="twitter:title" content={ogTitle} />
|
|
|
|
<Meta name="twitter:description" content={description} />
|
|
|
|
<Meta name="twitter:image" content={ogImage} />
|
|
|
|
|
2023-11-18 14:10:02 +00:00
|
|
|
<For each={imageUrls()}>{(imageUrl) => <Link rel="preload" as="image" href={imageUrl} />}</For>
|
2023-03-08 16:35:13 +00:00
|
|
|
<div class="wide-container">
|
2023-08-14 15:50:03 +00:00
|
|
|
<div class="row position-relative">
|
2023-11-13 16:55:32 +00:00
|
|
|
<article
|
2024-01-22 10:45:21 +00:00
|
|
|
ref={(el) => (articleContainer.current = el)}
|
2023-11-13 16:55:32 +00:00
|
|
|
class={clsx('col-md-16 col-lg-14 col-xl-12 offset-md-5', styles.articleContent)}
|
|
|
|
onClick={handleArticleBodyClick}
|
|
|
|
>
|
2023-07-02 05:08:42 +00:00
|
|
|
{/*TODO: Check styles.shoutTopic*/}
|
2023-11-28 13:18:25 +00:00
|
|
|
<Show when={props.article.layout !== 'audio'}>
|
2023-07-17 17:14:34 +00:00
|
|
|
<div class={styles.shoutHeader}>
|
|
|
|
<Show when={mainTopic()}>
|
2023-11-28 13:18:25 +00:00
|
|
|
<CardTopic title={mainTopic().title} slug={mainTopic().slug} />
|
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>
|
2023-12-19 09:34:24 +00:00
|
|
|
<a href={getPagePath(router, 'author', { slug: a.slug })}>{getAuthorName(a)}</a>
|
2023-07-17 17:14:34 +00:00
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
</div>
|
2023-07-28 09:47:19 +00:00
|
|
|
<Show
|
|
|
|
when={
|
|
|
|
props.article.cover &&
|
|
|
|
props.article.layout !== 'video' &&
|
|
|
|
props.article.layout !== 'image'
|
|
|
|
}
|
|
|
|
>
|
2023-12-08 11:49:37 +00:00
|
|
|
<figure class="img-align-column">
|
|
|
|
<Image width={800} alt={props.article.cover_caption} src={props.article.cover} />
|
2023-12-15 13:45:34 +00:00
|
|
|
<figcaption innerHTML={props.article.cover_caption} />
|
2023-12-08 11:49:37 +00:00
|
|
|
</figure>
|
2023-07-17 17:14:34 +00:00
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
</Show>
|
2023-08-22 13:37:54 +00:00
|
|
|
<Show when={props.article.lead}>
|
|
|
|
<section class={styles.lead} innerHTML={props.article.lead} />
|
|
|
|
</Show>
|
2023-11-28 13:18:25 +00:00
|
|
|
<Show when={props.article.layout === 'audio'}>
|
2023-07-17 17:14:34 +00:00
|
|
|
<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>
|
2023-07-02 05:08:42 +00:00
|
|
|
<Show when={media() && props.article.layout === 'video'}>
|
|
|
|
<div class="media-items">
|
|
|
|
<For each={media() || []}>
|
|
|
|
{(m: MediaItem) => (
|
|
|
|
<div class={styles.shoutMediaBody}>
|
2023-07-19 11:00:58 +00:00
|
|
|
<VideoPlayer
|
|
|
|
articleView={true}
|
|
|
|
videoUrl={m.url}
|
|
|
|
title={m.title}
|
|
|
|
description={m.body}
|
|
|
|
/>
|
2023-07-02 05:08:42 +00:00
|
|
|
<Show when={m?.body}>
|
2023-11-13 17:14:58 +00:00
|
|
|
<div innerHTML={m.body} />
|
2023-07-02 05:08:42 +00:00
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
</div>
|
|
|
|
</Show>
|
2023-07-18 11:26:32 +00:00
|
|
|
|
2023-07-02 05:08:42 +00:00
|
|
|
<Show when={body()}>
|
2023-11-13 22:05:04 +00:00
|
|
|
<div id="shoutBody" class={styles.shoutBody} innerHTML={body()} />
|
2023-07-02 05:08:42 +00:00
|
|
|
</Show>
|
2023-03-10 17:42:48 +00:00
|
|
|
</article>
|
2023-08-24 21:19:26 +00:00
|
|
|
|
2023-09-20 20:57:44 +00:00
|
|
|
<Show when={body()}>
|
2023-08-24 21:19:26 +00:00
|
|
|
<div class="col-md-6 offset-md-1">
|
|
|
|
<TableOfContents variant="article" parentSelector="#shoutBody" body={body()} />
|
|
|
|
</div>
|
2023-07-31 21:43:41 +00:00
|
|
|
</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">
|
2023-11-02 10:34:38 +00:00
|
|
|
<ImageSwiper images={media()} />
|
2023-07-17 22:24:37 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Show>
|
|
|
|
|
2023-03-23 17:54:33 +00:00
|
|
|
<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} />
|
2022-11-23 19:14:59 +00:00
|
|
|
</div>
|
2023-02-09 22:54:53 +00:00
|
|
|
|
2024-01-05 19:31:28 +00:00
|
|
|
<Popover content={t('Comment')} disabled={isActionPopupActive()}>
|
2023-05-17 04:04:38 +00:00
|
|
|
{(triggerRef: (el) => void) => (
|
2023-11-06 19:15:13 +00:00
|
|
|
<div class={clsx(styles.shoutStatsItem)} ref={triggerRef} onClick={scrollToComments}>
|
2023-05-17 04:04:38 +00:00
|
|
|
<Icon name="comment" class={styles.icon} />
|
2023-07-09 18:34:59 +00:00
|
|
|
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
|
2023-11-09 22:33:21 +00:00
|
|
|
<Show
|
|
|
|
when={props.article.stat?.commented}
|
|
|
|
fallback={<span class={styles.commentsTextLabel}>{t('Add comment')}</span>}
|
|
|
|
>
|
|
|
|
{props.article.stat?.commented}
|
|
|
|
</Show>
|
2023-05-17 04:04:38 +00:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Popover>
|
2023-07-09 18:34:59 +00:00
|
|
|
|
|
|
|
<Show when={props.article.stat?.viewed}>
|
|
|
|
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
|
2023-11-06 19:15:13 +00:00
|
|
|
{t('viewsWithCount', { count: props.article.stat?.viewed })}
|
2023-07-09 18:34:59 +00:00
|
|
|
</div>
|
|
|
|
</Show>
|
|
|
|
|
2023-11-06 19:15:13 +00:00
|
|
|
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalData)}>
|
|
|
|
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
|
|
|
|
{formattedDate()}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2024-01-05 19:31:28 +00:00
|
|
|
<Popover content={t('Add to bookmarks')} disabled={isActionPopupActive()}>
|
2023-11-06 19:15:13 +00:00
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<div
|
|
|
|
class={clsx(styles.shoutStatsItem, styles.shoutStatsItemBookmarks)}
|
|
|
|
ref={triggerRef}
|
|
|
|
onClick={handleBookmarkButtonClick}
|
|
|
|
>
|
|
|
|
<div class={styles.shoutStatsItemInner}>
|
|
|
|
<Icon name="bookmark" class={styles.icon} />
|
|
|
|
<Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Popover>
|
|
|
|
|
2024-01-05 19:31:28 +00:00
|
|
|
<Popover content={t('Share')} disabled={isActionPopupActive()}>
|
2023-05-17 04:04:38 +00:00
|
|
|
{(triggerRef: (el) => void) => (
|
|
|
|
<div class={styles.shoutStatsItem} ref={triggerRef}>
|
|
|
|
<SharePopup
|
|
|
|
title={props.article.title}
|
2023-12-13 10:39:31 +00:00
|
|
|
description={description}
|
2023-05-17 04:04:38 +00:00
|
|
|
imageUrl={props.article.cover}
|
2024-01-05 19:31:28 +00:00
|
|
|
shareUrl={shareUrl}
|
2023-05-17 04:04:38 +00:00
|
|
|
containerCssClass={stylesHeader.control}
|
2024-01-05 19:31:28 +00:00
|
|
|
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
|
2023-05-17 04:04:38 +00:00
|
|
|
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)} />
|
2023-05-17 04:04:38 +00:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Popover>
|
2023-11-06 19:15:13 +00:00
|
|
|
|
2023-03-10 17:42:48 +00:00
|
|
|
<Show when={canEdit()}>
|
2023-05-17 04:04:38 +00:00
|
|
|
<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)} />
|
2023-05-17 04:04:38 +00:00
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Popover>
|
2023-03-10 17:42:48 +00:00
|
|
|
</Show>
|
2023-11-06 19:15:13 +00:00
|
|
|
|
|
|
|
<FeedArticlePopup
|
|
|
|
isOwner={canEdit()}
|
|
|
|
containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
|
2024-01-05 19:31:28 +00:00
|
|
|
onShareClick={() => showModal('share')}
|
2024-01-25 09:57:57 +00:00
|
|
|
onInviteClick={() => showModal('inviteMembers')}
|
2024-01-05 19:31:28 +00:00
|
|
|
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
|
2023-11-06 19:15:13 +00:00
|
|
|
trigger={
|
|
|
|
<button>
|
|
|
|
<Icon name="ellipsis" class={clsx(styles.icon)} />
|
|
|
|
<Icon name="ellipsis" class={clsx(styles.icon, styles.iconHover)} />
|
|
|
|
</button>
|
|
|
|
}
|
|
|
|
/>
|
2023-03-10 17:42:48 +00:00
|
|
|
</div>
|
2023-11-06 19:15:13 +00:00
|
|
|
|
|
|
|
<Show when={isAuthenticated() && !canEdit()}>
|
|
|
|
<div class={styles.help}>
|
2023-03-10 17:42:48 +00:00
|
|
|
<button class="button">{t('Cooperate')}</button>
|
2023-11-06 19:15:13 +00:00
|
|
|
</div>
|
|
|
|
</Show>
|
|
|
|
<Show when={canEdit()}>
|
|
|
|
<div class={styles.help}>
|
2023-03-10 17:42:48 +00:00
|
|
|
<button class="button button--light">{t('Invite to collab')}</button>
|
2023-11-06 19:15:13 +00:00
|
|
|
</div>
|
|
|
|
</Show>
|
2023-03-10 17:42:48 +00:00
|
|
|
|
2023-05-26 11:30:27 +00:00
|
|
|
<Show when={props.article.topics.length}>
|
|
|
|
<div class={styles.topicsList}>
|
|
|
|
<For each={props.article.topics}>
|
|
|
|
{(topic) => (
|
|
|
|
<div class={styles.shoutTopic}>
|
2023-12-08 11:49:37 +00:00
|
|
|
<a href={getPagePath(router, 'topic', { slug: topic.slug })}>
|
2023-12-15 13:45:34 +00:00
|
|
|
{lang() === 'en' ? capitalize(topic.slug) : topic.title}
|
2023-12-08 11:49:37 +00:00
|
|
|
</a>
|
2023-05-26 11:30:27 +00:00
|
|
|
</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}>
|
2023-11-30 08:50:29 +00:00
|
|
|
{(a: Author) => (
|
2023-03-10 17:42:48 +00:00
|
|
|
<div class="col-xl-12">
|
2023-11-30 08:50:29 +00:00
|
|
|
<AuthorBadge iconButtons={true} showMessageButton={true} author={a} />
|
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}
|
2023-10-12 14:18:01 +00:00
|
|
|
articleAuthors={props.article.authors}
|
2023-03-10 17:42:48 +00:00
|
|
|
/>
|
|
|
|
</Show>
|
|
|
|
</div>
|
2023-02-09 22:54:53 +00:00
|
|
|
</div>
|
2022-09-09 11:53:35 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-11-13 08:05:05 +00:00
|
|
|
<Show when={selectedImage()}>
|
|
|
|
<Lightbox image={selectedImage()} onClose={handleLightboxClose} />
|
|
|
|
</Show>
|
2024-02-02 04:31:25 +00:00
|
|
|
<Modal variant="medium" name="inviteMembers">
|
|
|
|
<InviteMembers variant={'coauthors'} title={t('Invite experts')} />
|
|
|
|
</Modal>
|
2024-01-05 19:31:28 +00:00
|
|
|
<ShareModal
|
|
|
|
title={props.article.title}
|
|
|
|
description={description}
|
|
|
|
imageUrl={props.article.cover}
|
|
|
|
shareUrl={shareUrl}
|
|
|
|
/>
|
2023-02-09 22:54:53 +00:00
|
|
|
</>
|
2022-09-09 11:53:35 +00:00
|
|
|
)
|
|
|
|
}
|