webapp/src/components/Article/FullArticle.tsx

632 lines
22 KiB
TypeScript
Raw Normal View History

import { createPopper } from '@popperjs/core'
import { clsx } from 'clsx'
2024-06-24 17:50:27 +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'
import { isServer } from 'solid-js/web'
2024-06-24 17:50:27 +00:00
import { Link, Meta } from '@solidjs/meta'
import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session'
2024-06-24 17:50:27 +00:00
import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
2024-07-05 14:08:12 +00:00
import { isCyrillic } from '~/intl/translate'
import { getImageUrl, getOpenGraphImageUrl } from '~/lib/getImageUrl'
2024-06-24 17:50:27 +00:00
import { MediaItem } from '~/types/mediaitem'
import { capitalize } from '~/utils/capitalize'
2024-07-05 14:08:12 +00:00
import { getArticleDescription, getArticleKeywords } from '~/utils/meta'
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'
import { Icon } from '../_shared/Icon'
import { Image } from '../_shared/Image'
import { InviteMembers } from '../_shared/InviteMembers'
import { Lightbox } from '../_shared/Lightbox'
import { Popover } from '../_shared/Popover'
import { ShareModal } from '../_shared/ShareModal'
import { ImageSwiper } from '../_shared/SolidSwiper'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { AudioHeader } from './AudioHeader'
import { AudioPlayer } from './AudioPlayer'
import { CommentsTree } from './CommentsTree'
2024-02-04 11:25:21 +00:00
import { SharePopup, getShareUrl } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl'
2024-06-24 17:50:27 +00:00
import { A, useSearchParams } from '@solidjs/router'
import { useFeed } from '~/context/feed'
import stylesHeader from '../Nav/Header/Header.module.scss'
2024-02-04 11:25:21 +00:00
import styles from './Article.module.scss'
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
}
type IframeSize = {
width: number
height: number
}
export type ArticlePageSearchParams = {
scrollTo: 'comments'
commentId: string
slide?: string
}
const scrollTo = (el: HTMLElement) => {
2023-10-16 19:50:22 +00:00
const { top } = el.getBoundingClientRect()
2024-06-24 17:50:27 +00:00
if (window)
window.scrollTo({
top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
left: 0,
2024-06-26 08:22:05 +00:00
behavior: 'smooth'
2024-06-24 17:50:27 +00:00
})
}
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
export const FullArticle = (props: Props) => {
2024-06-24 17:50:27 +00:00
const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>()
const { showModal } = useUI()
2024-02-04 17:40:15 +00:00
const { loadReactionsBy } = useReactions()
2023-11-13 08:05:05 +00:00
const [selectedImage, setSelectedImage] = createSignal('')
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
2023-12-08 11:49:37 +00:00
const { t, formatDate, lang } = useLocalize()
2024-06-24 17:50:27 +00:00
const { session, requireAuthentication } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const { addSeen } = useFeed()
2024-06-24 17:50:27 +00:00
const formattedDate = createMemo(() => formatDate(new Date((props.article?.published_at || 0) * 1000)))
2024-02-16 17:04:05 +00:00
const canEdit = createMemo(
() =>
Boolean(author()?.id) &&
(props.article?.authors?.some((a) => Boolean(a) && a?.id === author().id) ||
props.article?.created_by?.id === author().id ||
2024-06-26 08:22:05 +00:00
session()?.user?.roles?.includes('editor'))
2024-02-16 10:21:25 +00:00
)
2022-09-09 11:53:35 +00:00
2023-12-08 11:49:37 +00:00
const mainTopic = createMemo(() => {
2024-06-24 17:50:27 +00:00
const mainTopicSlug = (props.article?.topics?.length || 0) > 0 ? props.article.main_topic : null
2024-07-06 06:24:37 +00:00
const mt = props.article?.topics?.find((tpc: Maybe<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-06-24 17:50:27 +00:00
return props.article?.topics?.[0]
2023-12-08 11:49:37 +00:00
})
2022-12-07 18:38:05 +00:00
2024-06-24 17:50:27 +00:00
const handleBookmarkButtonClick = (ev: MouseEvent | undefined) => {
requireAuthentication(() => {
// TODO: implement bookmark clicked
2024-06-24 17:50:27 +00:00
ev?.preventDefault()
}, 'bookmark')
2022-11-27 11:00:44 +00:00
}
const body = createMemo(() => {
2024-07-06 06:24:37 +00:00
if (props.article?.layout === 'literature') {
try {
2023-12-20 07:45:29 +00:00
if (props.article?.media) {
2024-07-06 06:24:37 +00:00
const media = JSON.parse(props.article?.media)
2023-12-20 07:45:29 +00:00
if (media.length > 0) {
return media[0].body
}
}
} catch (error) {
console.error(error)
}
}
2024-07-06 06:24:37 +00:00
return props.article?.body || ''
})
const imageUrls = createMemo(() => {
if (!body()) {
return []
}
if (isServer) {
const result: string[] = []
2024-06-24 17:50:27 +00:00
let match: RegExpMatchArray | null
while ((match = imgSrcRegExp.exec(body())) !== null) {
2024-06-24 17:50:27 +00:00
if (match) result.push(match[1])
else break
}
return result
}
const imageElements = document.querySelectorAll<HTMLImageElement>('#shoutBody img')
// eslint-disable-next-line unicorn/prefer-spread
return Array.from(imageElements).map((img) => img.src)
})
const media = createMemo<MediaItem[]>(() => {
try {
2024-02-17 14:31:08 +00:00
return JSON.parse(props.article?.media || '[]')
} catch {
return []
}
})
2022-11-27 11:00:44 +00:00
2024-06-24 17:50:27 +00:00
let commentsRef: HTMLDivElement | undefined
createEffect(() => {
2024-06-24 17:50:27 +00:00
if (searchParams?.commentId && isReactionsLoaded()) {
const commentElement = document.querySelector<HTMLElement>(
2024-06-26 08:22:05 +00:00
`[id='comment_${searchParams?.commentId}']`
)
2023-10-16 19:50:22 +00:00
if (commentElement) {
requestAnimationFrame(() => scrollTo(commentElement))
}
}
})
2024-06-24 17:50:27 +00:00
const clickHandlers: { element: HTMLElement; handler: () => void }[] = []
const documentClickHandlers: ((e: MouseEvent) => void)[] = []
createEffect(() => {
if (!body()) {
return
}
const tooltipElements: NodeListOf<HTMLElement> = document.querySelectorAll(
2024-06-26 08:22:05 +00:00
'[data-toggle="tooltip"], footnote'
)
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)
const tooltipContent = document.createElement('div')
tooltipContent.classList.add(styles.tooltipContent)
2024-06-24 17:50:27 +00:00
tooltipContent.innerHTML = element.dataset.originalTitle || element.dataset.value || ''
2024-01-23 16:44:58 +00:00
tooltip.append(tooltipContent)
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-09-07 18:45:22 +00:00
const popperInstance = createPopper(element, tooltip, {
placement: 'top',
modifiers: [
2023-09-07 07:25:02 +00:00
{
name: 'eventListeners',
2024-06-26 08:22:05 +00:00
options: { scroll: false }
2023-09-07 07:25:02 +00:00
},
{
name: 'offset',
options: {
2024-06-26 08:22:05 +00:00
offset: [0, 8]
}
2023-09-07 18:45:22 +00:00
},
{
name: 'flip',
2024-06-26 08:22:05 +00:00
options: { fallbackPlacements: ['top'] }
}
]
2023-08-17 11:11:58 +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()
}
2024-06-24 17:50:27 +00:00
const handleDocumentClick = (e: MouseEvent) => {
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
})
})
2024-06-24 17:50:27 +00:00
const openLightbox = (image: string) => {
2023-11-13 08:05:05 +00:00
setSelectedImage(image)
}
const handleLightboxClose = () => {
2024-06-24 17:50:27 +00:00
setSelectedImage('')
2023-11-13 08:05:05 +00:00
}
2024-06-24 17:50:27 +00:00
// biome-ignore lint/suspicious/noExplicitAny: FIXME: typing
const handleArticleBodyClick = (event: any) => {
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))
}
}
// Check iframes size
2024-06-24 17:50:27 +00:00
let articleContainer: HTMLElement | undefined
const updateIframeSizes = () => {
2024-06-24 17:50:27 +00:00
if (!(articleContainer && props.article.body && window)) return
const iframes = articleContainer?.querySelectorAll('iframe')
if (!iframes) return
2024-06-24 17:50:27 +00:00
const containerWidth = articleContainer?.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)
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`
}
})
}
createEffect(
on(
() => props.article,
() => {
updateIframeSizes()
2024-06-26 08:22:05 +00:00
}
)
)
onMount(async () => {
2024-06-24 17:50:27 +00:00
// install('G-LQ4B87H8C2')
2024-01-29 12:49:37 +00:00
await loadReactionsBy({ by: { shout: props.article.slug } })
2024-05-07 08:51:17 +00:00
addSeen(props.article.slug)
setIsReactionsLoaded(true)
document.title = props.article.title
window?.addEventListener('resize', updateIframeSizes)
onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
2024-04-30 15:58:27 +00:00
createEffect(() => {
2024-06-24 17:50:27 +00:00
if (props.scrollToComments && commentsRef) {
scrollTo(commentsRef)
2024-04-30 15:58:27 +00:00
}
})
createEffect(() => {
2024-06-24 17:50:27 +00:00
if (searchParams?.scrollTo === 'comments' && commentsRef) {
requestAnimationFrame(() => commentsRef && scrollTo(commentsRef))
changeSearchParams({ scrollTo: undefined })
2024-04-30 15:58:27 +00:00
}
})
})
const cover = props.article.cover ?? 'production/image/logo_image.png'
const ogImage = getOpenGraphImageUrl(cover, {
title: props.article.title,
2024-02-05 08:59:21 +00:00
topic: mainTopic()?.title || '',
2024-06-24 17:50:27 +00:00
author: props.article?.authors?.[0]?.name || '',
2024-06-26 08:22:05 +00:00
width: 1200
})
2024-07-05 14:08:12 +00:00
const description = getArticleDescription(props.article.description || body() || media()[0]?.body)
const ogTitle = props.article.title
2024-07-05 14:08:12 +00:00
const keywords = getArticleKeywords(props.article)
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
2023-12-19 09:34:24 +00:00
const getAuthorName = (a: Author) => {
2024-06-24 17:50:27 +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
<>
<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} />
<For each={imageUrls()}>{(imageUrl) => <Link rel="preload" as="image" href={imageUrl} />}</For>
2023-03-08 16:35:13 +00:00
<div class="wide-container">
<div class="row position-relative">
2023-11-13 16:55:32 +00:00
<article
2024-06-24 17:50:27 +00:00
ref={(el) => (articleContainer = 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}
>
{/*TODO: Check styles.shoutTopic*/}
2024-07-06 06:24:37 +00:00
<Show when={props.article?.layout !== 'audio'}>
2023-07-17 17:14:34 +00:00
<div class={styles.shoutHeader}>
<Show when={mainTopic()}>
2024-06-24 17:50:27 +00:00
<CardTopic title={mainTopic()?.title || ''} slug={mainTopic()?.slug || ''} />
2023-07-17 17:14:34 +00:00
</Show>
2024-07-06 06:24:37 +00:00
<h1>{props.article?.title || ''}</h1>
<Show when={props.article?.subtitle}>
<h4>{props.article?.subtitle || ''}</h4>
2023-07-17 17:14:34 +00:00
</Show>
<div class={styles.shoutAuthor}>
2024-07-06 06:24:37 +00:00
<For each={props.article?.authors}>
2024-06-24 17:50:27 +00:00
{(a: Maybe<Author>, index: () => number) => (
2023-07-17 17:14:34 +00:00
<>
<Show when={index() > 0}>, </Show>
2024-06-24 17:50:27 +00:00
<A href={`/author/${a?.slug}`}>{a && getAuthorName(a)}</A>
2023-07-17 17:14:34 +00:00
</>
)}
</For>
</div>
<Show
when={
2024-07-06 06:24:37 +00:00
props.article?.cover &&
props.article?.layout !== 'video' &&
props.article?.layout !== 'image'
}
>
2023-12-08 11:49:37 +00:00
<figure class="img-align-column">
2024-06-24 17:50:27 +00:00
<Image
width={800}
2024-07-06 06:24:37 +00:00
alt={props.article?.cover_caption || ''}
src={props.article?.cover || ''}
2024-06-24 17:50:27 +00:00
/>
2024-07-06 06:24:37 +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>
2024-07-06 06:24:37 +00:00
<Show when={props.article?.lead}>
<section class={styles.lead} innerHTML={props.article?.lead || ''} />
</Show>
2024-07-06 06:24:37 +00:00
<Show when={props.article?.layout === 'audio'}>
2023-07-17 17:14:34 +00:00
<AudioHeader
2024-07-06 06:24:37 +00:00
title={props.article?.title || ''}
cover={props.article?.cover || ''}
2023-07-17 17:14:34 +00:00
artistData={media()?.[0]}
2024-06-24 17:50:27 +00:00
topic={mainTopic() as Topic}
2023-07-17 17:14:34 +00:00
/>
<Show when={media().length > 0}>
<div class="media-items">
2024-07-06 06:24:37 +00:00
<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>
2024-07-06 06:24:37 +00:00
<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}>
2023-11-13 17:14:58 +00:00
<div innerHTML={m.body} />
</Show>
</div>
)}
</For>
</div>
</Show>
<Show when={body()}>
<div id="shoutBody" class={styles.shoutBody} innerHTML={body()} />
</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>
</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">
<ImageSwiper images={media()} />
2023-07-17 22:24:37 +00:00
</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')} disabled={isActionPopupActive()}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
<div
class={clsx(styles.shoutStatsItem)}
ref={triggerRef}
2024-06-24 17:50:27 +00:00
onClick={() => commentsRef && scrollTo(commentsRef)}
>
<Icon name="comment" class={styles.icon} />
2023-07-09 18:34:59 +00:00
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
<Show
when={props.article.stat?.commented}
fallback={<span class={styles.commentsTextLabel}>{t('Add comment')}</span>}
>
{props.article.stat?.commented}
</Show>
</div>
)}
</Popover>
2023-07-09 18:34:59 +00:00
<Show when={props.article.stat?.viewed}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
2024-06-24 17:50:27 +00:00
{t('some views', { count: props.article.stat?.viewed || 0 })}
2023-07-09 18:34:59 +00:00
</div>
</Show>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalData)}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
{formattedDate()}
</div>
</div>
<Popover content={t('Add to bookmarks')} disabled={isActionPopupActive()}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => 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>
<Popover content={t('Share')} disabled={isActionPopupActive()}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}>
<SharePopup
title={props.article.title}
description={description}
2024-06-24 17:50:27 +00:00
imageUrl={props.article.cover || ''}
shareUrl={shareUrl}
containerCssClass={stylesHeader.control}
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
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>
2023-03-10 17:42:48 +00:00
<Show when={canEdit()}>
<Popover content={t('Edit')}>
2024-06-24 17:50:27 +00:00
{(triggerRef: (el: HTMLElement) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}>
2024-06-24 17:50:27 +00:00
<A href={`/edit/${props.article?.id}`} 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)} />
2024-06-24 17:50:27 +00:00
</A>
</div>
)}
</Popover>
2023-03-10 17:42:48 +00:00
</Show>
<FeedArticlePopup
2024-06-24 17:50:27 +00:00
canEdit={Boolean(canEdit())}
containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
onShareClick={() => showModal('share')}
onInviteClick={() => showModal('inviteMembers')}
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
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>
2024-05-03 08:36:15 +00:00
<Show when={author()?.id && !canEdit()}>
<div class={styles.help}>
2023-03-10 17:42:48 +00:00
<button class="button">{t('Cooperate')}</button>
</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>
</div>
</Show>
2023-03-10 17:42:48 +00:00
2024-07-06 06:24:37 +00:00
<Show when={props.article?.topics?.length}>
<div class={styles.topicsList}>
2024-07-06 06:24:37 +00:00
<For each={props.article?.topics || []}>
{(topic) => (
<div class={styles.shoutTopic}>
2024-06-24 17:50:27 +00:00
<A href={`/topic/${topic?.slug || ''}`}>
{lang() === 'en' ? capitalize(topic?.slug || '') : topic?.title || ''}
</A>
</div>
)}
</For>
</div>
</Show>
2023-03-10 17:42:48 +00:00
<div class={styles.shoutAuthorsList}>
2024-06-24 17:50:27 +00:00
<Show when={(props.article.authors?.length || 0) > 1}>
2023-03-10 17:42:48 +00:00
<h4>{t('Authors')}</h4>
</Show>
<For each={props.article.authors}>
2024-06-24 17:50:27 +00:00
{(a: Maybe<Author>) => (
2023-03-10 17:42:48 +00:00
<div class="col-xl-12">
2024-06-24 17:50:27 +00:00
<AuthorBadge iconButtons={true} showMessageButton={true} author={a as Author} />
2023-03-10 17:42:48 +00:00
</div>
)}
</For>
</div>
2024-06-24 17:50:27 +00:00
<div id="comments" ref={(el) => (commentsRef = el)}>
2023-03-10 17:42:48 +00:00
<Show when={isReactionsLoaded()}>
<CommentsTree
shoutId={props.article.id}
shoutSlug={props.article.slug}
2024-06-24 17:50:27 +00:00
articleAuthors={props.article.authors as Author[]}
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>
<Modal variant="medium" name="inviteMembers">
<InviteMembers variant={'coauthors'} title={t('Invite experts')} />
</Modal>
<ShareModal
title={props.article.title}
description={description}
2024-06-24 17:50:27 +00:00
imageUrl={props.article.cover || ''}
shareUrl={shareUrl}
/>
2023-02-09 22:54:53 +00:00
</>
2022-09-09 11:53:35 +00:00
)
}