meta-refactored
This commit is contained in:
parent
b204204a31
commit
e3ac3cc406
|
@ -1,6 +1,6 @@
|
||||||
// import { install } from 'ga-gtag'
|
// import { install } from 'ga-gtag'
|
||||||
import { createPopper } from '@popperjs/core'
|
import { createPopper } from '@popperjs/core'
|
||||||
import { Link, Meta } from '@solidjs/meta'
|
import { Link } from '@solidjs/meta'
|
||||||
import { A, useSearchParams } from '@solidjs/router'
|
import { A, useSearchParams } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
|
@ -12,10 +12,9 @@ import { useSession } from '~/context/session'
|
||||||
import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
|
import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
|
||||||
import type { Author, Maybe, QueryLoad_Reactions_ByArgs, Shout, Topic } from '~/graphql/schema/core.gen'
|
import type { Author, Maybe, QueryLoad_Reactions_ByArgs, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||||
import { isCyrillic } from '~/intl/translate'
|
import { isCyrillic } from '~/intl/translate'
|
||||||
import { getImageUrl, getOpenGraphImageUrl } from '~/lib/getImageUrl'
|
import { getImageUrl } from '~/lib/getImageUrl'
|
||||||
import { MediaItem } from '~/types/mediaitem'
|
import { MediaItem } from '~/types/mediaitem'
|
||||||
import { capitalize } from '~/utils/capitalize'
|
import { capitalize } from '~/utils/capitalize'
|
||||||
import { getArticleDescription, getArticleKeywords } from '~/utils/meta'
|
|
||||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||||
import { CardTopic } from '../Feed/CardTopic'
|
import { CardTopic } from '../Feed/CardTopic'
|
||||||
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
|
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
|
||||||
|
@ -302,7 +301,6 @@ export const FullArticle = (props: Props) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// install('G-LQ4B87H8C2')
|
|
||||||
const opts: QueryLoad_Reactions_ByArgs = { by: { shout: props.article.slug }, limit: 999, offset: 0 }
|
const opts: QueryLoad_Reactions_ByArgs = { by: { shout: props.article.slug }, limit: 999, offset: 0 }
|
||||||
const _rrr = await loadReactionsBy(opts)
|
const _rrr = await loadReactionsBy(opts)
|
||||||
addSeen(props.article.slug)
|
addSeen(props.article.slug)
|
||||||
|
@ -326,34 +324,11 @@ export const FullArticle = (props: Props) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const cover = props.article.cover || 'production/image/logo_image.png'
|
|
||||||
const ogImage = getOpenGraphImageUrl(cover, {
|
|
||||||
title: props.article.title,
|
|
||||||
topic: mainTopic()?.title || '',
|
|
||||||
author: props.article.authors?.[0]?.name || '',
|
|
||||||
width: 1200
|
|
||||||
})
|
|
||||||
|
|
||||||
const description = getArticleDescription(props.article.description || body() || media()[0]?.body)
|
|
||||||
const ogTitle = props.article.title
|
|
||||||
const keywords = getArticleKeywords(props.article)
|
|
||||||
const shareUrl = getShareUrl({ pathname: `/${props.article.slug || ''}` })
|
const shareUrl = getShareUrl({ pathname: `/${props.article.slug || ''}` })
|
||||||
const getAuthorName = (a: Author) => {
|
const getAuthorName = (a: Author) =>
|
||||||
return lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name
|
lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<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>
|
<For each={imageUrls()}>{(imageUrl) => <Link rel="preload" as="image" href={imageUrl} />}</For>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row position-relative">
|
<div class="row position-relative">
|
||||||
|
@ -522,7 +497,7 @@ export const FullArticle = (props: Props) => {
|
||||||
<div class={styles.shoutStatsItem} ref={triggerRef}>
|
<div class={styles.shoutStatsItem} ref={triggerRef}>
|
||||||
<SharePopup
|
<SharePopup
|
||||||
title={props.article.title}
|
title={props.article.title}
|
||||||
description={description}
|
description={props.article.description || body() || media()[0]?.body}
|
||||||
imageUrl={props.article.cover || ''}
|
imageUrl={props.article.cover || ''}
|
||||||
shareUrl={shareUrl}
|
shareUrl={shareUrl}
|
||||||
containerCssClass={stylesHeader.control}
|
containerCssClass={stylesHeader.control}
|
||||||
|
@ -623,7 +598,7 @@ export const FullArticle = (props: Props) => {
|
||||||
</Modal>
|
</Modal>
|
||||||
<ShareModal
|
<ShareModal
|
||||||
title={props.article.title}
|
title={props.article.title}
|
||||||
description={description}
|
description={props.article.description || body() || media()[0]?.body}
|
||||||
imageUrl={props.article.cover || ''}
|
imageUrl={props.article.cover || ''}
|
||||||
shareUrl={shareUrl}
|
shareUrl={shareUrl}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { For, Show, createEffect, createSignal, on } from 'solid-js'
|
|
||||||
import { useAuthors } from '~/context/authors'
|
|
||||||
import { useLocalize } from '~/context/localize'
|
|
||||||
import { loadAuthors } from '~/graphql/api/public'
|
|
||||||
import { Author } from '~/graphql/schema/core.gen'
|
|
||||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
|
||||||
import { InlineLoader } from '../InlineLoader'
|
|
||||||
import { AUTHORS_PER_PAGE } from '../Views/AllAuthors/AllAuthors'
|
|
||||||
import { Button } from '../_shared/Button'
|
|
||||||
import styles from './AuthorsList.module.scss'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
class?: string
|
|
||||||
query: 'followers' | 'shouts'
|
|
||||||
searchQuery?: string
|
|
||||||
allAuthorsLength?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// pagination handling, loadAuthors cached from api, addAuthors to context
|
|
||||||
|
|
||||||
export const AuthorsList = (props: Props) => {
|
|
||||||
const { t } = useLocalize()
|
|
||||||
const { addAuthors } = useAuthors()
|
|
||||||
const [authorsByShouts, setAuthorsByShouts] = createSignal<Author[]>()
|
|
||||||
const [authorsByFollowers, setAuthorsByFollowers] = createSignal<Author[]>()
|
|
||||||
const [loading, setLoading] = createSignal(false)
|
|
||||||
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
|
|
||||||
const [allLoaded, setAllLoaded] = createSignal(false)
|
|
||||||
|
|
||||||
const fetchAuthors = async (queryType: Props['query'], page: number) => {
|
|
||||||
setLoading(true)
|
|
||||||
const offset = AUTHORS_PER_PAGE * page
|
|
||||||
const fetcher = await loadAuthors({
|
|
||||||
by: { order: queryType },
|
|
||||||
limit: AUTHORS_PER_PAGE,
|
|
||||||
offset
|
|
||||||
})
|
|
||||||
const result = await fetcher()
|
|
||||||
if (result) {
|
|
||||||
addAuthors([...result])
|
|
||||||
if (queryType === 'shouts') {
|
|
||||||
setAuthorsByShouts((prev) => [...(prev || []), ...result])
|
|
||||||
} else if (queryType === 'followers') {
|
|
||||||
setAuthorsByFollowers((prev) => [...(prev || []), ...result])
|
|
||||||
}
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMoreAuthors = () => {
|
|
||||||
const nextPage = currentPage()[props.query] + 1
|
|
||||||
fetchAuthors(props.query, nextPage).then(() =>
|
|
||||||
setCurrentPage({ ...currentPage(), [props.query]: nextPage })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => props.query,
|
|
||||||
(query) => {
|
|
||||||
const al = query === 'shouts' ? authorsByShouts() : authorsByFollowers()
|
|
||||||
if (al?.length === 0 && currentPage()[query] === 0) {
|
|
||||||
setCurrentPage((prev) => ({ ...prev, [query]: 0 }))
|
|
||||||
fetchAuthors(query, 0).then(() => setCurrentPage((prev) => ({ ...prev, [query]: 1 })))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const authorsList = () => (props.query === 'shouts' ? authorsByShouts() : authorsByFollowers())
|
|
||||||
createEffect(() => setAllLoaded(props.allAuthorsLength === authorsList.length))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={clsx(styles.AuthorsList, props.class)}>
|
|
||||||
<For each={authorsList()}>
|
|
||||||
{(author) => (
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-20 col-xl-18">
|
|
||||||
<AuthorBadge author={author} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-20 col-xl-18">
|
|
||||||
<div class={styles.action}>
|
|
||||||
<Show when={!loading() && (authorsList()?.length || 0) > 0 && !allLoaded()}>
|
|
||||||
<Button value={t('Load more')} onClick={loadMoreAuthors} />
|
|
||||||
</Show>
|
|
||||||
<Show when={loading() && !allLoaded()}>
|
|
||||||
<InlineLoader />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { AuthorsList } from './AuthorsList'
|
|
|
@ -7,7 +7,6 @@ import { useTopics } from '~/context/topics'
|
||||||
import { useUI } from '~/context/ui'
|
import { useUI } from '~/context/ui'
|
||||||
import type { Topic } from '../../../graphql/schema/core.gen'
|
import type { Topic } from '../../../graphql/schema/core.gen'
|
||||||
import { getRandomTopicsFromArray } from '../../../lib/getRandomTopicsFromArray'
|
import { getRandomTopicsFromArray } from '../../../lib/getRandomTopicsFromArray'
|
||||||
import { getArticleDescription } from '../../../utils/meta'
|
|
||||||
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Newsletter } from '../../_shared/Newsletter'
|
import { Newsletter } from '../../_shared/Newsletter'
|
||||||
|
@ -24,7 +23,7 @@ type Props = {
|
||||||
title?: string
|
title?: string
|
||||||
slug?: string
|
slug?: string
|
||||||
isHeaderFixed?: boolean
|
isHeaderFixed?: boolean
|
||||||
articleBody?: string
|
desc?: string
|
||||||
cover?: string
|
cover?: string
|
||||||
scrollToComments?: (value: boolean) => void
|
scrollToComments?: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
@ -324,10 +323,8 @@ export const Header = (props: Props) => {
|
||||||
title={props.title || ''}
|
title={props.title || ''}
|
||||||
imageUrl={props.cover || ''}
|
imageUrl={props.cover || ''}
|
||||||
shareUrl={getShareUrl()}
|
shareUrl={getShareUrl()}
|
||||||
description={getArticleDescription(props.articleBody?.slice(0, 100) || '')}
|
description={props.desc || ''}
|
||||||
onVisibilityChange={(isVisible) => {
|
onVisibilityChange={setIsSharePopupVisible}
|
||||||
setIsSharePopupVisible(isVisible)
|
|
||||||
}}
|
|
||||||
containerCssClass={styles.control}
|
containerCssClass={styles.control}
|
||||||
trigger={
|
trigger={
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export { Topics } from './Topics'
|
|
|
@ -3,9 +3,9 @@ import { Icon } from '~/components/_shared/Icon'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
|
|
||||||
import { A, useMatch } from '@solidjs/router'
|
import { A, useMatch } from '@solidjs/router'
|
||||||
import styles from './Topics.module.scss'
|
import styles from './TopicsNav.module.scss'
|
||||||
|
|
||||||
export const Topics = () => {
|
export const TopicsNav = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const matchExpo = useMatch(() => '/expo')
|
const matchExpo = useMatch(() => '/expo')
|
||||||
return (
|
return (
|
1
src/components/Nav/TopicsNav/index.ts
Normal file
1
src/components/Nav/TopicsNav/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { TopicsNav } from './TopicsNav'
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Author, Topic } from '~/graphql/schema/core.gen'
|
import type { Author, Topic } from '~/graphql/schema/core.gen'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createSignal } from 'solid-js'
|
import { Show, createEffect, createMemo, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '~/context/following'
|
import { useFollowing } from '~/context/following'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
|
@ -9,6 +9,7 @@ import { useSession } from '~/context/session'
|
||||||
import { FollowingEntity } from '~/graphql/schema/core.gen'
|
import { FollowingEntity } from '~/graphql/schema/core.gen'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
|
|
||||||
|
import { capitalize } from '~/utils/capitalize'
|
||||||
import { FollowingCounters } from '../_shared/FollowingCounters/FollowingCounters'
|
import { FollowingCounters } from '../_shared/FollowingCounters/FollowingCounters'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import styles from './Full.module.scss'
|
import styles from './Full.module.scss'
|
||||||
|
@ -20,11 +21,21 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullTopic = (props: Props) => {
|
export const FullTopic = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { follows, changeFollowing } = useFollowing()
|
const { follows, changeFollowing } = useFollowing()
|
||||||
const { requireAuthentication } = useSession()
|
const { requireAuthentication } = useSession()
|
||||||
const [followed, setFollowed] = createSignal()
|
const [followed, setFollowed] = createSignal()
|
||||||
|
|
||||||
|
const title = createMemo(
|
||||||
|
() =>
|
||||||
|
// FIXME: use title translation
|
||||||
|
`#${capitalize(
|
||||||
|
lang() === 'en'
|
||||||
|
? props.topic.slug.replace(/-/, ' ')
|
||||||
|
: props.topic.title || props.topic.slug.replace(/-/, ' '),
|
||||||
|
true
|
||||||
|
)}`
|
||||||
|
)
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (follows?.topics?.length !== 0) {
|
if (follows?.topics?.length !== 0) {
|
||||||
const items = follows.topics || []
|
const items = follows.topics || []
|
||||||
|
@ -42,7 +53,7 @@ export const FullTopic = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
|
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
|
||||||
<h1>#{props.topic?.title}</h1>
|
<h1>{title()}</h1>
|
||||||
<p class={styles.topicDescription} innerHTML={props.topic?.body || ''} />
|
<p class={styles.topicDescription} innerHTML={props.topic?.body || ''} />
|
||||||
|
|
||||||
<div class={styles.topicDetails}>
|
<div class={styles.topicDetails}>
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { useSearchParams } from '@solidjs/router'
|
import { useSearchParams } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
import { AuthorBadge } from '~/components/Author/AuthorBadge'
|
||||||
|
import { InlineLoader } from '~/components/InlineLoader'
|
||||||
|
import { Button } from '~/components/_shared/Button'
|
||||||
import { Loading } from '~/components/_shared/Loading'
|
import { Loading } from '~/components/_shared/Loading'
|
||||||
import { SearchField } from '~/components/_shared/SearchField'
|
import { SearchField } from '~/components/_shared/SearchField'
|
||||||
import { useAuthors } from '~/context/authors'
|
import { useAuthors } from '~/context/authors'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import type { Author } from '~/graphql/schema/core.gen'
|
import type { Author } from '~/graphql/schema/core.gen'
|
||||||
import enKeywords from '~/intl/locales/en/keywords.json'
|
|
||||||
import ruKeywords from '~/intl/locales/ru/keywords.json'
|
|
||||||
import { authorLetterReduce, translateAuthor } from '~/intl/translate'
|
import { authorLetterReduce, translateAuthor } from '~/intl/translate'
|
||||||
import { dummyFilter } from '~/lib/dummyFilter'
|
import { dummyFilter } from '~/lib/dummyFilter'
|
||||||
import { getImageUrl } from '~/lib/getImageUrl'
|
|
||||||
import { scrollHandler } from '~/utils/scroll'
|
import { scrollHandler } from '~/utils/scroll'
|
||||||
import { AuthorsList } from '../../AuthorsList'
|
|
||||||
import styles from './AllAuthors.module.scss'
|
import styles from './AllAuthors.module.scss'
|
||||||
|
import stylesAuthorList from './AuthorsList.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authors: Author[]
|
authors: Author[]
|
||||||
topFollowedAuthors?: Author[]
|
authorsByFollowers?: Author[]
|
||||||
topWritingAuthors?: Author[]
|
authorsByShouts?: Author[]
|
||||||
isLoaded: boolean
|
isLoaded: boolean
|
||||||
}
|
}
|
||||||
export const AUTHORS_PER_PAGE = 20
|
export const AUTHORS_PER_PAGE = 20
|
||||||
|
@ -34,8 +33,9 @@ export const AllAuthors = (props: Props) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const alphabet = createMemo(() => ABC[lang()] || ABC['ru'])
|
const alphabet = createMemo(() => ABC[lang()] || ABC['ru'])
|
||||||
const [searchParams, changeSearchParams] = useSearchParams<{ by?: string }>()
|
const [searchParams, changeSearchParams] = useSearchParams<{ by?: string }>()
|
||||||
const { authorsSorted, setAuthorsSort } = useAuthors()
|
const { authorsSorted, setAuthorsSort, loadAuthors } = useAuthors()
|
||||||
const authors = createMemo(() => props.authors || authorsSorted())
|
const authors = createMemo(() => props.authors || authorsSorted())
|
||||||
|
const [loading, setLoading] = createSignal<boolean>(false)
|
||||||
|
|
||||||
// filter
|
// filter
|
||||||
const [searchQuery, setSearchQuery] = createSignal('')
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
|
@ -52,7 +52,8 @@ export const AllAuthors = (props: Props) => {
|
||||||
|
|
||||||
// store by first char
|
// store by first char
|
||||||
const byLetterFiltered = createMemo<{ [letter: string]: Author[] }>(() => {
|
const byLetterFiltered = createMemo<{ [letter: string]: Author[] }>(() => {
|
||||||
console.debug('[components.AllAuthors] byLetterFiltered')
|
if (!(filteredAuthors()?.length > 0)) return {}
|
||||||
|
console.debug('[components.AllAuthors] update byLetterFiltered', filteredAuthors()?.length)
|
||||||
return (
|
return (
|
||||||
filteredAuthors()?.reduce(
|
filteredAuthors()?.reduce(
|
||||||
(acc, author: Author) => authorLetterReduce(acc, author, lang()),
|
(acc, author: Author) => authorLetterReduce(acc, author, lang()),
|
||||||
|
@ -69,120 +70,164 @@ export const AllAuthors = (props: Props) => {
|
||||||
return keys
|
return keys
|
||||||
})
|
})
|
||||||
|
|
||||||
const ogImage = createMemo(() => getImageUrl('production/image/logo_image.png'))
|
const fetchAuthors = async (queryType: string, page: number) => {
|
||||||
const ogTitle = createMemo(() => t('Authors'))
|
try {
|
||||||
const description = createMemo(() => t('List of authors of the open editorial community'))
|
console.debug('[components.AuthorsList] fetching authors...')
|
||||||
|
setLoading(true)
|
||||||
return (
|
setAuthorsSort?.(queryType)
|
||||||
<div class={clsx([styles.allAuthorsPage, 'wide-container'])}>
|
const offset = AUTHORS_PER_PAGE * page
|
||||||
<Meta name="descprition" content={description() || ''} />
|
await loadAuthors({
|
||||||
<Meta name="keywords" content={lang() === 'ru' ? ruKeywords[''] : enKeywords['']} />
|
by: { order: queryType },
|
||||||
<Meta name="og:type" content="article" />
|
limit: AUTHORS_PER_PAGE,
|
||||||
<Meta name="og:title" content={ogTitle() || ''} />
|
offset
|
||||||
<Meta name="og:image" content={ogImage() || ''} />
|
})
|
||||||
<Meta name="twitter:image" content={ogImage() || ''} />
|
} catch (error) {
|
||||||
<Meta name="og:description" content={description() || ''} />
|
console.error('[components.AuthorsList] error fetching authors:', error)
|
||||||
<Meta name="twitter:card" content="summary_large_image" />
|
} finally {
|
||||||
<Meta name="twitter:title" content={ogTitle() || ''} />
|
setLoading(false)
|
||||||
<Meta name="twitter:description" content={description() || ''} />
|
}
|
||||||
<Show when={props.isLoaded} fallback={<Loading />}>
|
}
|
||||||
<div class="offset-md-5">
|
const [currentPage, setCurrentPage] = createSignal<{ followers: number; shouts: number }>({
|
||||||
<div class="row">
|
followers: 0,
|
||||||
<div class="col-lg-20 col-xl-18">
|
shouts: 0
|
||||||
<h1>{t('Authors')}</h1>
|
})
|
||||||
<p>{t('Subscribe who you like to tune your personal feed')}</p>
|
const loadMoreAuthors = () => {
|
||||||
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
|
const by = searchParams?.by as 'followers' | 'shouts' | undefined
|
||||||
<li
|
if (!by) return
|
||||||
class={clsx({
|
const nextPage = currentPage()[by] + 1
|
||||||
['view-switcher__item--selected']: !searchParams?.by || searchParams?.by === 'shouts'
|
fetchAuthors(by, nextPage).then(() => setCurrentPage({ ...currentPage(), [by]: nextPage }))
|
||||||
})}
|
}
|
||||||
>
|
|
||||||
<a href="/author?by=shouts">{t('By shouts')}</a>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class={clsx({
|
|
||||||
['view-switcher__item--selected']: searchParams?.by === 'followers'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<a href="/author?by=followers">{t('By popularity')}</a>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class={clsx({
|
|
||||||
['view-switcher__item--selected']: searchParams?.by === 'name'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<a href="/author?by=name">{t('By name')}</a>
|
|
||||||
</li>
|
|
||||||
<Show when={searchParams?.by === 'name'}>
|
|
||||||
<li class="view-switcher__search">
|
|
||||||
<SearchField onChange={(value) => setSearchQuery(value)} />
|
|
||||||
</li>
|
|
||||||
</Show>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
const TabNavigator = () => (
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-20 col-xl-18">
|
||||||
|
<h1>{t('Authors')}</h1>
|
||||||
|
<p>{t('Subscribe who you like to tune your personal feed')}</p>
|
||||||
|
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
|
||||||
|
<li
|
||||||
|
class={clsx({
|
||||||
|
['view-switcher__item--selected']: !searchParams?.by || searchParams?.by === 'shouts'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a href="/author?by=shouts">{t('By shouts')}</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class={clsx({
|
||||||
|
['view-switcher__item--selected']: searchParams?.by === 'followers'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a href="/author?by=followers">{t('By popularity')}</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class={clsx({
|
||||||
|
['view-switcher__item--selected']: searchParams?.by === 'name'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a href="/author?by=name">{t('By name')}</a>
|
||||||
|
</li>
|
||||||
<Show when={searchParams?.by === 'name'}>
|
<Show when={searchParams?.by === 'name'}>
|
||||||
|
<li class="view-switcher__search">
|
||||||
|
<SearchField onChange={(value) => setSearchQuery(value)} />
|
||||||
|
</li>
|
||||||
|
</Show>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AbcNavigator = () => (
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-20 col-xl-18">
|
||||||
|
<ul class={clsx('nodash', styles.alphabet)}>
|
||||||
|
<For each={[...(alphabet() || [])]}>
|
||||||
|
{(letter, index) => (
|
||||||
|
<li>
|
||||||
|
<Show when={letter in byLetterFiltered()} fallback={letter}>
|
||||||
|
<a
|
||||||
|
href={`/author?by=name#letter-${index()}`}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollHandler(`letter-${index()}`)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{letter}
|
||||||
|
</a>
|
||||||
|
</Show>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AbcAuthorsList = () => (
|
||||||
|
<For each={sortedKeys() || []}>
|
||||||
|
{(letter) => (
|
||||||
|
<div class={clsx(styles.group, 'group')}>
|
||||||
|
<h2 id={`letter-${alphabet()?.indexOf(letter) || ''}`}>{letter}</h2>
|
||||||
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-20 col-xl-18">
|
<div class="col-lg-20">
|
||||||
<ul class={clsx('nodash', styles.alphabet)}>
|
<div class="row">
|
||||||
<For each={[...(alphabet() || [])]}>
|
<For each={byLetterFiltered()?.[letter] || []}>
|
||||||
{(letter, index) => (
|
{(author) => (
|
||||||
<li>
|
<div class={clsx(styles.topic, 'topic col-sm-12 col-md-8')}>
|
||||||
<Show when={letter in byLetterFiltered()} fallback={letter}>
|
<div class="topic-title">
|
||||||
<a
|
<a href={`/author/${author.slug}`}>{translateAuthor(author, lang())}</a>
|
||||||
href={`/author?by=name#letter-${index()}`}
|
<Show when={author.stat?.shouts || 0}>
|
||||||
onClick={(event) => {
|
<span class={styles.articlesCounter}>{author.stat?.shouts || 0}</span>
|
||||||
event.preventDefault()
|
</Show>
|
||||||
scrollHandler(`letter-${index()}`)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{letter}
|
|
||||||
</a>
|
|
||||||
</Show>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<For each={sortedKeys() || []}>
|
|
||||||
{(letter) => (
|
|
||||||
<div class={clsx(styles.group, 'group')}>
|
|
||||||
<h2 id={`letter-${alphabet()?.indexOf(letter) || ''}`}>{letter}</h2>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-20">
|
|
||||||
<div class="row">
|
|
||||||
<For each={byLetterFiltered()?.[letter] || []}>
|
|
||||||
{(author) => (
|
|
||||||
<div class={clsx(styles.topic, 'topic col-sm-12 col-md-8')}>
|
|
||||||
<div class="topic-title">
|
|
||||||
<a href={`/author/${author.slug}`}>{translateAuthor(author, lang())}</a>
|
|
||||||
<Show when={author.stat?.shouts || 0}>
|
|
||||||
<span class={styles.articlesCounter}>{author.stat?.shouts || 0}</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</For>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
<Show when={authors().length && searchParams?.by !== 'name' && props.isLoaded}>
|
</div>
|
||||||
<AuthorsList
|
)}
|
||||||
allAuthorsLength={authors().length}
|
</For>
|
||||||
searchQuery={searchQuery()}
|
)
|
||||||
query={searchParams?.by === 'followers' ? 'followers' : 'shouts'}
|
|
||||||
/>
|
const AuthorsSortedList = () => (
|
||||||
|
<div class={clsx(stylesAuthorList.AuthorsList)}>
|
||||||
|
<For each={authorsSorted?.()}>
|
||||||
|
{(author) => (
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-20 col-xl-18">
|
||||||
|
<AuthorBadge author={author} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-20 col-xl-18">
|
||||||
|
<div class={stylesAuthorList.action}>
|
||||||
|
<Show when={!loading() && ((authorsSorted?.() || []).length || 0) > 0}>
|
||||||
|
<Button value={t('Load more')} onClick={loadMoreAuthors} aria-live="polite" />
|
||||||
|
</Show>
|
||||||
|
<Show when={loading()}>
|
||||||
|
<InlineLoader />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={props.isLoaded} fallback={<Loading />}>
|
||||||
|
<div class="offset-md-5">
|
||||||
|
<TabNavigator />
|
||||||
|
|
||||||
|
<Show when={searchParams?.by === 'name'} fallback={<AuthorsSortedList />}>
|
||||||
|
<AbcNavigator />
|
||||||
|
<AbcAuthorsList />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { A, useSearchParams } from '@solidjs/router'
|
import { A, useSearchParams } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
@ -7,10 +6,7 @@ import { SearchField } from '~/components/_shared/SearchField'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useTopics } from '~/context/topics'
|
import { useTopics } from '~/context/topics'
|
||||||
import type { Topic } from '~/graphql/schema/core.gen'
|
import type { Topic } from '~/graphql/schema/core.gen'
|
||||||
import enKeywords from '~/intl/locales/en/keywords.json'
|
|
||||||
import ruKeywords from '~/intl/locales/ru/keywords.json'
|
|
||||||
import { dummyFilter } from '~/lib/dummyFilter'
|
import { dummyFilter } from '~/lib/dummyFilter'
|
||||||
import { getImageUrl } from '~/lib/getImageUrl'
|
|
||||||
import { capitalize } from '~/utils/capitalize'
|
import { capitalize } from '~/utils/capitalize'
|
||||||
import { scrollHandler } from '~/utils/scroll'
|
import { scrollHandler } from '~/utils/scroll'
|
||||||
import { TopicBadge } from '../../Topic/TopicBadge'
|
import { TopicBadge } from '../../Topic/TopicBadge'
|
||||||
|
@ -98,26 +94,9 @@ export const AllTopics = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
// meta
|
|
||||||
const ogImage = getImageUrl('production/image/logo_image.png')
|
|
||||||
const ogTitle = t('Themes and plots')
|
|
||||||
const description = t(
|
|
||||||
'Thematic table of contents of the magazine. Here you can find all the topics that the community authors wrote about'
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.allTopicsPage, 'wide-container')}>
|
<>
|
||||||
<Meta name="descprition" content={description} />
|
<h1>{t('Themes and plots')}</h1>
|
||||||
<Meta name="keywords" content={lang() === 'ru' ? ruKeywords[''] : enKeywords['']} />
|
|
||||||
<Meta name="og:type" content="article" />
|
|
||||||
<Meta name="og:title" content={ogTitle} />
|
|
||||||
<Meta name="og:image" content={ogImage} />
|
|
||||||
<Meta name="twitter: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} />
|
|
||||||
<Show when={Boolean(filteredResults())} fallback={<Loading />}>
|
<Show when={Boolean(filteredResults())} fallback={<Loading />}>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-19 offset-md-5">
|
<div class="col-md-19 offset-md-5">
|
||||||
|
@ -199,6 +178,6 @@ export const AllTopics = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Meta, Title } from '@solidjs/meta'
|
|
||||||
import { A, useLocation, useParams } from '@solidjs/router'
|
import { A, useLocation, useParams } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
@ -10,14 +9,12 @@ import { useGraphQL } from '~/context/graphql'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useSession } from '~/context/session'
|
import { useSession } from '~/context/session'
|
||||||
import { useUI } from '~/context/ui'
|
import { useUI } from '~/context/ui'
|
||||||
|
import { loadReactions } from '~/graphql/api/public'
|
||||||
import loadShoutsQuery from '~/graphql/query/core/articles-load-by'
|
import loadShoutsQuery from '~/graphql/query/core/articles-load-by'
|
||||||
import getAuthorFollowersQuery from '~/graphql/query/core/author-followers'
|
import getAuthorFollowersQuery from '~/graphql/query/core/author-followers'
|
||||||
import getAuthorFollowsQuery from '~/graphql/query/core/author-follows'
|
import getAuthorFollowsQuery from '~/graphql/query/core/author-follows'
|
||||||
import loadReactionsBy from '~/graphql/query/core/reactions-load-by'
|
|
||||||
import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen'
|
import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||||
import { getImageUrl } from '~/lib/getImageUrl'
|
|
||||||
import { byCreated } from '~/lib/sortby'
|
import { byCreated } from '~/lib/sortby'
|
||||||
import { getArticleDescription } from '~/utils/meta'
|
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
||||||
import { splitToPages } from '~/utils/splitToPages'
|
import { splitToPages } from '~/utils/splitToPages'
|
||||||
import stylesArticle from '../../Article/Article.module.scss'
|
import stylesArticle from '../../Article/Article.module.scss'
|
||||||
|
@ -57,7 +54,7 @@ export const AuthorView = (props: Props) => {
|
||||||
const [followers, setFollowers] = createSignal<Author[]>([] as Author[])
|
const [followers, setFollowers] = createSignal<Author[]>([] as Author[])
|
||||||
const [following, changeFollowing] = createSignal<Array<Author | Topic>>([] as Array<Author | Topic>) // flat AuthorFollowsResult
|
const [following, changeFollowing] = createSignal<Array<Author | Topic>>([] as Array<Author | Topic>) // flat AuthorFollowsResult
|
||||||
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
|
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
|
||||||
const [commented, setCommented] = createSignal<Reaction[]>()
|
const [commented, setCommented] = createSignal<Reaction[]>([])
|
||||||
const { query } = useGraphQL()
|
const { query } = useGraphQL()
|
||||||
|
|
||||||
// пагинация загрузки ленты постов
|
// пагинация загрузки ленты постов
|
||||||
|
@ -123,11 +120,11 @@ export const AuthorView = (props: Props) => {
|
||||||
if (!commented() && profile) {
|
if (!commented() && profile) {
|
||||||
await loadMore()
|
await loadMore()
|
||||||
|
|
||||||
const resp = await query(loadReactionsBy, {
|
const commentsFetcher = loadReactions({
|
||||||
by: { comment: true, created_by: profile.id }
|
by: { comment: true, created_by: profile.id }
|
||||||
}).toPromise()
|
})
|
||||||
const ccc = resp?.data?.load_reactions_by
|
const ccc = await commentsFetcher()
|
||||||
if (ccc) setCommented(ccc)
|
if (ccc) setCommented((_) => ccc || [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// { defer: true },
|
// { defer: true },
|
||||||
|
@ -150,31 +147,12 @@ export const AuthorView = (props: Props) => {
|
||||||
const pages = createMemo<Shout[][]>(() =>
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
||||||
)
|
)
|
||||||
|
|
||||||
const ogImage = createMemo(() =>
|
|
||||||
author()?.pic
|
|
||||||
? getImageUrl(author()?.pic || '', { width: 1200 })
|
|
||||||
: getImageUrl('production/image/logo_image.png')
|
|
||||||
)
|
|
||||||
const description = createMemo(() => getArticleDescription(author()?.bio || ''))
|
|
||||||
const handleDeleteComment = (id: number) => {
|
const handleDeleteComment = (id: number) => {
|
||||||
setCommented((prev) => (prev || []).filter((comment) => comment.id !== id))
|
setCommented((prev) => (prev || []).filter((comment) => comment.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.authorPage}>
|
<div class={styles.authorPage}>
|
||||||
<Show when={author()}>
|
|
||||||
<Title>{author()?.name}</Title>
|
|
||||||
<Meta name="descprition" content={description()} />
|
|
||||||
<Meta name="og:type" content="profile" />
|
|
||||||
<Meta name="og:title" content={author()?.name || ''} />
|
|
||||||
<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={author()?.name || ''} />
|
|
||||||
<Meta name="twitter:description" content={description()} />
|
|
||||||
<Meta name="twitter:image" content={ogImage()} />
|
|
||||||
</Show>
|
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<Show when={author()} fallback={<Loading />}>
|
<Show when={author()} fallback={<Loading />}>
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { A, createAsync, useLocation, useNavigate, useSearchParams } from '@solidjs/router'
|
import { A, createAsync, useLocation, useNavigate, useSearchParams } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
|
||||||
|
@ -17,9 +16,6 @@ import { useTopics } from '~/context/topics'
|
||||||
import { useUI } from '~/context/ui'
|
import { useUI } from '~/context/ui'
|
||||||
import { loadUnratedShouts } from '~/graphql/api/private'
|
import { loadUnratedShouts } from '~/graphql/api/private'
|
||||||
import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
|
import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
|
||||||
import ruKeywords from '~/intl/locales/ru/keywords.json'
|
|
||||||
import enKeywords from '~/intl/locales/ru/keywords.json'
|
|
||||||
import { getImageUrl } from '~/lib/getImageUrl'
|
|
||||||
import { byCreated } from '~/lib/sortby'
|
import { byCreated } from '~/lib/sortby'
|
||||||
import { FeedSearchParams } from '~/routes/feed/[feed]'
|
import { FeedSearchParams } from '~/routes/feed/[feed]'
|
||||||
import { CommentDate } from '../../Article/CommentDate'
|
import { CommentDate } from '../../Article/CommentDate'
|
||||||
|
@ -41,7 +37,7 @@ export type FeedProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeedView = (props: FeedProps) => {
|
export const FeedView = (props: FeedProps) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const loc = useLocation()
|
const loc = useLocation()
|
||||||
const client = useGraphQL()
|
const client = useGraphQL()
|
||||||
const unrated = createAsync(async () => {
|
const unrated = createAsync(async () => {
|
||||||
|
@ -79,12 +75,6 @@ export const FeedView = (props: FeedProps) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const ogImage = getImageUrl('production/image/logo_image.png')
|
|
||||||
const description = createMemo(() =>
|
|
||||||
t('Independent media project about culture, science, art and society with horizontal editing')
|
|
||||||
)
|
|
||||||
const ogTitle = createMemo(() => t('Feed'))
|
|
||||||
|
|
||||||
const [shareData, setShareData] = createSignal<Shout | undefined>()
|
const [shareData, setShareData] = createSignal<Shout | undefined>()
|
||||||
const handleShare = (shared: Shout | undefined) => {
|
const handleShare = (shared: Shout | undefined) => {
|
||||||
showModal('share')
|
showModal('share')
|
||||||
|
@ -92,17 +82,7 @@ export const FeedView = (props: FeedProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="wide-container feed">
|
<div class="feed">
|
||||||
<Meta name="descprition" content={description()} />
|
|
||||||
<Meta name="keywords" content={lang() === 'ru' ? ruKeywords[''] : enKeywords['']} />
|
|
||||||
<Meta name="og:type" content="article" />
|
|
||||||
<Meta name="og:title" content={ogTitle()} />
|
|
||||||
<Meta name="og:image" content={ogImage} />
|
|
||||||
<Meta name="twitter: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()} />
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}>
|
<div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
||||||
|
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { useAuthors } from '~/context/authors'
|
import { useAuthors } from '~/context/authors'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useTopics } from '~/context/topics'
|
import { useTopics } from '~/context/topics'
|
||||||
import { loadShouts } from '~/graphql/api/public'
|
import { loadShouts } from '~/graphql/api/public'
|
||||||
import { Author, Shout, Topic } from '~/graphql/schema/core.gen'
|
import { Author, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||||
import enKeywords from '~/intl/locales/en/keywords.json'
|
|
||||||
import ruKeywords from '~/intl/locales/ru/keywords.json'
|
|
||||||
import { SHOUTS_PER_PAGE } from '~/routes/(home)'
|
import { SHOUTS_PER_PAGE } from '~/routes/(home)'
|
||||||
import { capitalize } from '~/utils/capitalize'
|
import { capitalize } from '~/utils/capitalize'
|
||||||
import { splitToPages } from '~/utils/splitToPages'
|
import { splitToPages } from '~/utils/splitToPages'
|
||||||
|
@ -20,7 +16,7 @@ import { Row2 } from '../Feed/Row2'
|
||||||
import { Row3 } from '../Feed/Row3'
|
import { Row3 } from '../Feed/Row3'
|
||||||
import { Row5 } from '../Feed/Row5'
|
import { Row5 } from '../Feed/Row5'
|
||||||
import RowShort from '../Feed/RowShort'
|
import RowShort from '../Feed/RowShort'
|
||||||
import { Topics } from '../Nav/Topics'
|
import { TopicsNav } from '../Nav/TopicsNav'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
|
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
|
||||||
import styles from './Home.module.scss'
|
import styles from './Home.module.scss'
|
||||||
|
@ -40,7 +36,7 @@ export interface HomeViewProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HomeView = (props: HomeViewProps) => {
|
export const HomeView = (props: HomeViewProps) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { topAuthors, addAuthors } = useAuthors()
|
const { topAuthors, addAuthors } = useAuthors()
|
||||||
const { topTopics, randomTopic } = useTopics()
|
const { topTopics, randomTopic } = useTopics()
|
||||||
const [randomTopicArticles, setRandomTopicArticles] = createSignal<Shout[]>([])
|
const [randomTopicArticles, setRandomTopicArticles] = createSignal<Shout[]>([])
|
||||||
|
@ -75,9 +71,8 @@ export const HomeView = (props: HomeViewProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta name="keywords" content={`${lang() === 'ru' ? ruKeywords[''] : enKeywords['']}`} />
|
|
||||||
<Show when={(props.featuredShouts || []).length > 0}>
|
<Show when={(props.featuredShouts || []).length > 0}>
|
||||||
<Topics />
|
<TopicsNav />
|
||||||
<Row5 articles={props.featuredShouts.slice(0, 5)} nodate={true} />
|
<Row5 articles={props.featuredShouts.slice(0, 5)} nodate={true} />
|
||||||
<Hero />
|
<Hero />
|
||||||
<Show when={props.featuredShouts?.length > SHOUTS_PER_PAGE}>
|
<Show when={props.featuredShouts?.length > SHOUTS_PER_PAGE}>
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
import { JSX, onMount } from 'solid-js'
|
||||||
import { JSX, createMemo, onMount } from 'solid-js'
|
|
||||||
import { useLocalize } from '~/context/localize'
|
|
||||||
import enKeywords from '~/intl/locales/en/keywords.json'
|
|
||||||
import ruKeywords from '~/intl/locales/ru/keywords.json'
|
|
||||||
import { processPrepositions } from '~/intl/prepositions'
|
import { processPrepositions } from '~/intl/prepositions'
|
||||||
import { getImageUrl } from '~/lib/getImageUrl'
|
|
||||||
import { TableOfContents } from '../TableOfContents'
|
import { TableOfContents } from '../TableOfContents'
|
||||||
import { PageLayout } from '../_shared/PageLayout'
|
import { PageLayout } from '../_shared/PageLayout'
|
||||||
|
|
||||||
|
@ -15,39 +10,16 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StaticPage = (props: Props) => {
|
export const StaticPage = (props: Props) => {
|
||||||
let articleBodyElement: HTMLElement | null = null
|
let bodyEl: HTMLElement | undefined
|
||||||
const { t, lang } = useLocalize()
|
|
||||||
const ogTitle = createMemo(() => t(props.title || 'Discours'))
|
|
||||||
const description = createMemo(() => t(props.desc || ''))
|
|
||||||
const ogImage = getImageUrl('production/image/logo_image.png')
|
|
||||||
const keywords = createMemo(() => {
|
|
||||||
const page = props.title.toLocaleLowerCase() as keyof typeof ruKeywords
|
|
||||||
return `${lang() === 'ru' ? ruKeywords[page] : enKeywords[page]}`
|
|
||||||
})
|
|
||||||
let bodyEl: HTMLDivElement | undefined
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (bodyEl) bodyEl.innerHTML = processPrepositions(bodyEl.innerHTML)
|
if (bodyEl) bodyEl.innerHTML = processPrepositions(bodyEl.innerHTML)
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<PageLayout title={props.title}>
|
<PageLayout title={props.title} desc={props.desc} key={props.title.toLowerCase()}>
|
||||||
<Meta name="descprition" content={description()} />
|
<article class="wide-container container--static-page" id="articleBody" ref={(el) => (bodyEl = el)}>
|
||||||
<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="twitter: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()} />
|
|
||||||
<article
|
|
||||||
class="wide-container container--static-page"
|
|
||||||
id="articleBody"
|
|
||||||
ref={(el) => (articleBodyElement = el)}
|
|
||||||
>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 col-xl-14 offset-md-5 order-md-first mt-5" ref={(el) => (bodyEl = el)}>
|
<div class="col-md-12 col-xl-14 offset-md-5 order-md-first mt-5">
|
||||||
<h1>{ogTitle()}</h1>
|
<h1>{props.title}</h1>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -55,7 +27,7 @@ export const StaticPage = (props: Props) => {
|
||||||
<TableOfContents
|
<TableOfContents
|
||||||
variant="article"
|
variant="article"
|
||||||
parentSelector="#articleBody"
|
parentSelector="#articleBody"
|
||||||
body={(articleBodyElement as unknown as HTMLElement)?.outerHTML}
|
body={(bodyEl as unknown as HTMLElement)?.outerHTML}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
|
import { useSearchParams } from '@solidjs/router'
|
||||||
|
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useSearchParams } from '@solidjs/router'
|
|
||||||
import { useAuthors } from '~/context/authors'
|
import { useAuthors } from '~/context/authors'
|
||||||
import { useFeed } from '~/context/feed'
|
import { useFeed } from '~/context/feed'
|
||||||
import { useGraphQL } from '~/context/graphql'
|
import { useGraphQL } from '~/context/graphql'
|
||||||
|
@ -14,12 +10,8 @@ import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-t
|
||||||
import loadShoutsRandomQuery from '~/graphql/query/core/articles-load-random-topic'
|
import loadShoutsRandomQuery from '~/graphql/query/core/articles-load-random-topic'
|
||||||
import loadAuthorsByQuery from '~/graphql/query/core/authors-load-by'
|
import loadAuthorsByQuery from '~/graphql/query/core/authors-load-by'
|
||||||
import getTopicFollowersQuery from '~/graphql/query/core/topic-followers'
|
import getTopicFollowersQuery from '~/graphql/query/core/topic-followers'
|
||||||
import enKeywords from '~/intl/locales/en/keywords.json'
|
import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||||
import ruKeywords from '~/intl/locales/ru/keywords.json'
|
|
||||||
import { getImageUrl } from '~/lib/getImageUrl'
|
|
||||||
import { capitalize } from '~/utils/capitalize'
|
|
||||||
import { getUnixtime } from '~/utils/getServerDate'
|
import { getUnixtime } from '~/utils/getServerDate'
|
||||||
import { getArticleDescription } from '~/utils/meta'
|
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
||||||
import { splitToPages } from '~/utils/splitToPages'
|
import { splitToPages } from '~/utils/splitToPages'
|
||||||
import styles from '../../styles/Topic.module.scss'
|
import styles from '../../styles/Topic.module.scss'
|
||||||
|
@ -45,7 +37,7 @@ export const PRERENDERED_ARTICLES_COUNT = 28
|
||||||
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
||||||
|
|
||||||
export const TopicView = (props: Props) => {
|
export const TopicView = (props: Props) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { query } = useGraphQL()
|
const { query } = useGraphQL()
|
||||||
const [searchParams, changeSearchParams] = useSearchParams<TopicsPageSearchParams>()
|
const [searchParams, changeSearchParams] = useSearchParams<TopicsPageSearchParams>()
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
@ -55,7 +47,6 @@ export const TopicView = (props: Props) => {
|
||||||
const { authorsByTopic } = useAuthors()
|
const { authorsByTopic } = useAuthors()
|
||||||
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
|
||||||
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
const [topic, setTopic] = createSignal<Topic>()
|
const [topic, setTopic] = createSignal<Topic>()
|
||||||
createEffect(
|
createEffect(
|
||||||
on([() => props.topicSlug, topic, topicEntities], async ([slug, t, ttt]) => {
|
on([() => props.topicSlug, topic, topicEntities], async ([slug, t, ttt]) => {
|
||||||
|
@ -113,16 +104,6 @@ export const TopicView = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = createMemo(
|
|
||||||
() =>
|
|
||||||
`#${capitalize(
|
|
||||||
lang() === 'en'
|
|
||||||
? (topic() as Topic)?.slug.replace(/-/, ' ')
|
|
||||||
: (topic() as Topic)?.title || (topic() as Topic)?.slug.replace(/-/, ' '),
|
|
||||||
true
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
saveScrollPosition()
|
saveScrollPosition()
|
||||||
|
|
||||||
|
@ -154,31 +135,8 @@ export const TopicView = (props: Props) => {
|
||||||
const pages = createMemo<Shout[][]>(() =>
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
||||||
)
|
)
|
||||||
|
|
||||||
const ogImage = () =>
|
|
||||||
topic()?.pic
|
|
||||||
? getImageUrl(topic()?.pic || '', { width: 1200 })
|
|
||||||
: getImageUrl('production/image/logo_image.png')
|
|
||||||
const description = () =>
|
|
||||||
topic()?.body
|
|
||||||
? getArticleDescription(topic()?.body || '')
|
|
||||||
: t('The most interesting publications on the topic', { topicName: title() })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.topicPage}>
|
<div class={styles.topicPage}>
|
||||||
<Meta name="descprition" content={description()} />
|
|
||||||
<Meta
|
|
||||||
name="keywords"
|
|
||||||
content={`${title()}, ${lang() === 'ru' ? ruKeywords['topic'] : enKeywords['topic']}`}
|
|
||||||
/>
|
|
||||||
<Meta name="og:type" content="article" />
|
|
||||||
<Meta name="og:title" content={title()} />
|
|
||||||
<Meta name="og:image" content={ogImage()} />
|
|
||||||
<Meta name="twitter:image" content={ogImage()} />
|
|
||||||
<Meta name="og:description" content={description()} />
|
|
||||||
<Meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<Meta name="twitter:title" content={title()} />
|
|
||||||
<Meta name="twitter:description" content={description()} />
|
|
||||||
<FullTopic topic={topic() as Topic} followers={followers()} authors={topicAuthors()} />
|
<FullTopic topic={topic() as Topic} followers={followers()} authors={topicAuthors()} />
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class={clsx(styles.groupControls, 'row group__controls')}>
|
<div class={clsx(styles.groupControls, 'row group__controls')}>
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
import type { JSX } from 'solid-js'
|
import { Meta, Title } from '@solidjs/meta'
|
||||||
|
import { useLocation } from '@solidjs/router'
|
||||||
import { Title } from '@solidjs/meta'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createSignal } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
|
import { Show, createEffect, createMemo, createSignal } from 'solid-js'
|
||||||
|
import { useLocalize } from '~/context/localize'
|
||||||
|
import { Shout } from '~/graphql/schema/core.gen'
|
||||||
|
import enKeywords from '~/intl/locales/en/keywords.json'
|
||||||
|
import ruKeywords from '~/intl/locales/ru/keywords.json'
|
||||||
|
import { getImageUrl, getOpenGraphImageUrl } from '~/lib/getImageUrl'
|
||||||
|
import { getArticleKeywords } from '~/utils/meta'
|
||||||
import { FooterView } from '../Discours/Footer'
|
import { FooterView } from '../Discours/Footer'
|
||||||
import { Header } from '../Nav/Header'
|
import { Header } from '../Nav/Header'
|
||||||
|
|
||||||
import '../../styles/app.scss'
|
|
||||||
import styles from './PageLayout.module.scss'
|
import styles from './PageLayout.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type PageLayoutProps = {
|
||||||
title: string
|
title: string
|
||||||
|
desc?: string
|
||||||
headerTitle?: string
|
headerTitle?: string
|
||||||
slug?: string
|
slug?: string
|
||||||
articleBody?: string
|
article?: Shout
|
||||||
cover?: string
|
cover?: string
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
isHeaderFixed?: boolean
|
isHeaderFixed?: boolean
|
||||||
|
@ -23,29 +27,56 @@ type Props = {
|
||||||
withPadding?: boolean
|
withPadding?: boolean
|
||||||
zeroBottomPadding?: boolean
|
zeroBottomPadding?: boolean
|
||||||
scrollToComments?: (value: boolean) => void
|
scrollToComments?: (value: boolean) => void
|
||||||
|
key?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageLayout = (props: Props) => {
|
export const PageLayout = (props: PageLayoutProps) => {
|
||||||
const isHeaderFixed = props.isHeaderFixed === undefined ? true : props.isHeaderFixed
|
const isHeaderFixed = props.isHeaderFixed === undefined ? true : props.isHeaderFixed // FIXME: выглядит как костылек
|
||||||
|
const loc = useLocation()
|
||||||
|
const { t, lang } = useLocalize()
|
||||||
|
const imageUrl = props.cover ? getImageUrl(props.cover) : 'production/image/logo_image.png'
|
||||||
|
const ogImage = createMemo(() =>
|
||||||
|
// NOTE: preview generation logic works only for one article view
|
||||||
|
props.article
|
||||||
|
? getOpenGraphImageUrl(imageUrl, {
|
||||||
|
title: props.title,
|
||||||
|
topic: props.article?.topics?.[0]?.title || '',
|
||||||
|
author: props.article?.authors?.[0]?.name || '',
|
||||||
|
width: 1200
|
||||||
|
})
|
||||||
|
: imageUrl
|
||||||
|
)
|
||||||
|
const ogTitle = createMemo(() => t(props.title))
|
||||||
|
const description = createMemo(() => (props.desc ? t(props.desc) : ''))
|
||||||
|
const keypath = createMemo(() => (props.key || loc?.pathname.split('/')[0]) as keyof typeof ruKeywords)
|
||||||
|
const keywords = createMemo(
|
||||||
|
() =>
|
||||||
|
(props.article && getArticleKeywords(props.article as Shout)) ||
|
||||||
|
(lang() === 'ru' ? ruKeywords[keypath()] : enKeywords[keypath()])
|
||||||
|
)
|
||||||
const [scrollToComments, setScrollToComments] = createSignal<boolean>(false)
|
const [scrollToComments, setScrollToComments] = createSignal<boolean>(false)
|
||||||
|
createEffect(() => props.scrollToComments?.(scrollToComments()))
|
||||||
createEffect(() => {
|
|
||||||
if (props.scrollToComments) {
|
|
||||||
props.scrollToComments(scrollToComments())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>{props.title}</Title>
|
<Title>{props.title}</Title>
|
||||||
<Header
|
<Header
|
||||||
slug={props.slug}
|
slug={props.slug}
|
||||||
title={props.headerTitle}
|
title={props.headerTitle}
|
||||||
articleBody={props.articleBody}
|
desc={props.desc}
|
||||||
cover={props.articleBody}
|
cover={imageUrl}
|
||||||
isHeaderFixed={isHeaderFixed}
|
isHeaderFixed={isHeaderFixed}
|
||||||
scrollToComments={(value) => setScrollToComments(value)}
|
scrollToComments={(value) => setScrollToComments(value)}
|
||||||
/>
|
/>
|
||||||
|
<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="twitter: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() || ''} />
|
||||||
<main
|
<main
|
||||||
class={clsx('main-content', {
|
class={clsx('main-content', {
|
||||||
[styles.withPadding]: props.withPadding,
|
[styles.withPadding]: props.withPadding,
|
||||||
|
@ -53,7 +84,7 @@ export const PageLayout = (props: Props) => {
|
||||||
})}
|
})}
|
||||||
classList={{ 'main-content--no-padding': !isHeaderFixed }}
|
classList={{ 'main-content--no-padding': !isHeaderFixed }}
|
||||||
>
|
>
|
||||||
{props.children}
|
<div class={clsx([props.class, 'wide-container'])}>{props.children}</div>
|
||||||
</main>
|
</main>
|
||||||
<Show when={props.hideFooter !== true}>
|
<Show when={props.hideFooter !== true}>
|
||||||
<FooterView />
|
<FooterView />
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
const { mutation } = useGraphQL()
|
const { mutation } = useGraphQL()
|
||||||
|
|
||||||
const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise<Reaction[]> => {
|
const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise<Reaction[]> => {
|
||||||
const fetcher = await loadReactions({ ...opts })
|
const fetcher = await loadReactions(opts)
|
||||||
const result = (await fetcher()) || []
|
const result = (await fetcher()) || []
|
||||||
const newReactionsByShout: Record<string, Reaction[]> = {}
|
const newReactionsByShout: Record<string, Reaction[]> = {}
|
||||||
const newReactionEntities = result.reduce(
|
const newReactionEntities = result.reduce(
|
||||||
|
|
|
@ -60,7 +60,7 @@ export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => {
|
||||||
const filter = new URLSearchParams(options.by as Record<string, string>)
|
const filter = new URLSearchParams(options.by as Record<string, string>)
|
||||||
console.debug(options)
|
console.debug(options)
|
||||||
return cache(async () => {
|
return cache(async () => {
|
||||||
const resp = await defaultClient.query(loadReactionsByQuery, { ...options }).toPromise()
|
const resp = await defaultClient.query(loadReactionsByQuery, options).toPromise()
|
||||||
const result = resp?.data?.load_reactions_by
|
const result = resp?.data?.load_reactions_by
|
||||||
if (result) return result as Reaction[]
|
if (result) return result as Reaction[]
|
||||||
}, `reactions-${filter}-${page}`)
|
}, `reactions-${filter}-${page}`)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"dogma": "Discours.io, dogma, editorial principles, code of ethics, journalism, community",
|
"dogma": "Discours.io, dogma, editorial principles, code of ethics, journalism, community",
|
||||||
"guide": "discours.io, guide, help, how to start, reference, tutorial",
|
"guide": "discours.io, guide, help, how to start, reference, tutorial",
|
||||||
"": "Discours.io, Discours magazine, Discours, culture, science, art, society, independent journalism, literature, music, cinema, video, photography",
|
"home": "Discours.io, Discours magazine, Discours, culture, science, art, society, independent journalism, literature, music, cinema, video, photography",
|
||||||
"principles": "Discours.io, communities, values, editorial rules, polyphony, creation",
|
"principles": "Discours.io, communities, values, editorial rules, polyphony, creation",
|
||||||
"terms-of-use": "Discours.io, site rules, terms of use",
|
"terms-of-use": "Discours.io, site rules, terms of use",
|
||||||
"topic": "{topic}, Discours.io, articles, journalism, research"
|
"topic": "{topic}, Discours.io, articles, journalism, research"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"dogma": "discours.io, догма, принципы редактирования, этический кодекс, журналистика, сообщество",
|
"dogma": "discours.io, догма, принципы редактирования, этический кодекс, журналистика, сообщество",
|
||||||
"guide": "discours.io, гид, помощь, как начать, справочник, туториал",
|
"guide": "discours.io, гид, помощь, как начать, справочник, туториал",
|
||||||
"": "discours.io, Дискурс журнал, Дискурс, культура, наука, искусство, общество, независимая журналистика, литература, музыка, кино, видео, фотография",
|
"home": "discours.io, Дискурс журнал, Дискурс, культура, наука, искусство, общество, независимая журналистика, литература, музыка, кино, видео, фотография",
|
||||||
"principles": "discours.io, сообщества, ценности, принципы редактировани, плюрализм мнений, сотворчество",
|
"principles": "discours.io, сообщества, ценности, принципы редактировани, плюрализм мнений, сотворчество",
|
||||||
"terms-of-use": "discours.io, правила сайта, правила, пользовательское соглашение",
|
"terms-of-use": "discours.io, правила сайта, правила, пользовательское соглашение",
|
||||||
"topic": "discours.io, Дискурс, статьи, журналистика, исследование"
|
"topic": "discours.io, Дискурс, статьи, журналистика, исследование"
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default function HomePage(props: RouteSectionProps<HomeViewProps>) {
|
||||||
onMount(async () => await loadMoreFeatured())
|
onMount(async () => await loadMoreFeatured())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout withPadding={true} title={t('Discours')}>
|
<PageLayout withPadding={true} title={t('Discours')} key={'home'}>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<HomeView {...(data() as HomeViewProps)} />
|
<HomeView {...(data() as HomeViewProps)} />
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { useLocalize } from '~/context/localize'
|
||||||
import { getShout } from '~/graphql/api/public'
|
import { getShout } from '~/graphql/api/public'
|
||||||
import type { Shout } from '~/graphql/schema/core.gen'
|
import type { Shout } from '~/graphql/schema/core.gen'
|
||||||
import { initGA, loadGAScript } from '~/utils/ga'
|
import { initGA, loadGAScript } from '~/utils/ga'
|
||||||
|
import { getArticleKeywords } from '~/utils/meta'
|
||||||
import { FullArticle } from '../components/Article/FullArticle'
|
import { FullArticle } from '../components/Article/FullArticle'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { ReactionsProvider } from '../context/reactions'
|
import { ReactionsProvider } from '../context/reactions'
|
||||||
|
@ -91,9 +92,9 @@ export default (props: RouteSectionProps<{ article: Shout }>) => {
|
||||||
<Show when={article()?.id} fallback={<Loading />}>
|
<Show when={article()?.id} fallback={<Loading />}>
|
||||||
<PageLayout
|
<PageLayout
|
||||||
title={title()}
|
title={title()}
|
||||||
|
desc={getArticleKeywords(article() as Shout)}
|
||||||
headerTitle={article()?.title || ''}
|
headerTitle={article()?.title || ''}
|
||||||
slug={article()?.slug}
|
slug={article()?.slug}
|
||||||
articleBody={article()?.body}
|
|
||||||
cover={article()?.cover || ''}
|
cover={article()?.cover || ''}
|
||||||
scrollToComments={(value) => setScrollToComments(value)}
|
scrollToComments={(value) => setScrollToComments(value)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { RouteDefinition, RouteLoadFuncArgs, type RouteSectionProps, createAsync
|
||||||
import { Suspense, createEffect, on } from 'solid-js'
|
import { Suspense, createEffect, on } from 'solid-js'
|
||||||
import { AllAuthors } from '~/components/Views/AllAuthors'
|
import { AllAuthors } from '~/components/Views/AllAuthors'
|
||||||
import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors'
|
import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors'
|
||||||
|
import styles from '~/components/Views/AllAuthors/AllAuthors.module.scss'
|
||||||
import { Loading } from '~/components/_shared/Loading'
|
import { Loading } from '~/components/_shared/Loading'
|
||||||
import { PageLayout } from '~/components/_shared/PageLayout'
|
import { PageLayout } from '~/components/_shared/PageLayout'
|
||||||
import { useAuthors } from '~/context/authors'
|
import { useAuthors } from '~/context/authors'
|
||||||
|
@ -27,13 +28,13 @@ export const route = {
|
||||||
const isAll = !by || by === 'name'
|
const isAll = !by || by === 'name'
|
||||||
return {
|
return {
|
||||||
authors: isAll && (await fetchAllAuthors()),
|
authors: isAll && (await fetchAllAuthors()),
|
||||||
topFollowedAuthors: await fetchAuthorsWithStat(10, 'followers'),
|
authorsByFollowers: await fetchAuthorsWithStat(10, 'followers'),
|
||||||
topShoutsAuthors: await fetchAuthorsWithStat(10, 'shouts')
|
authorsByShouts: await fetchAuthorsWithStat(10, 'shouts')
|
||||||
} as AllAuthorsData
|
} as AllAuthorsData
|
||||||
}
|
}
|
||||||
} satisfies RouteDefinition
|
} satisfies RouteDefinition
|
||||||
|
|
||||||
type AllAuthorsData = { authors: Author[]; topFollowedAuthors: Author[]; topShoutsAuthors: Author[] }
|
type AllAuthorsData = { authors: Author[]; authorsByFollowers: Author[]; authorsByShouts: Author[] }
|
||||||
|
|
||||||
// addAuthors to context
|
// addAuthors to context
|
||||||
|
|
||||||
|
@ -46,8 +47,8 @@ export default function AllAuthorsPage(props: RouteSectionProps<AllAuthorsData>)
|
||||||
if (props.data) return props.data
|
if (props.data) return props.data
|
||||||
return {
|
return {
|
||||||
authors: await fetchAllAuthors(),
|
authors: await fetchAllAuthors(),
|
||||||
topFollowedAuthors: await fetchAuthorsWithStat(10, 'followers'),
|
authorsByFollowers: await fetchAuthorsWithStat(10, 'followers'),
|
||||||
topShoutsAuthors: await fetchAuthorsWithStat(10, 'shouts')
|
authorsByShouts: await fetchAuthorsWithStat(10, 'shouts')
|
||||||
} as AllAuthorsData
|
} as AllAuthorsData
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -58,8 +59,8 @@ export default function AllAuthorsPage(props: RouteSectionProps<AllAuthorsData>)
|
||||||
([data, aa]) => {
|
([data, aa]) => {
|
||||||
if (data && aa) {
|
if (data && aa) {
|
||||||
aa(data.authors as Author[])
|
aa(data.authors as Author[])
|
||||||
aa(data.topFollowedAuthors as Author[])
|
aa(data.authorsByFollowers as Author[])
|
||||||
aa(data.topShoutsAuthors as Author[])
|
aa(data.authorsByShouts as Author[])
|
||||||
console.debug('[routes.author] added all authors:', data.authors)
|
console.debug('[routes.author] added all authors:', data.authors)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -68,14 +69,19 @@ export default function AllAuthorsPage(props: RouteSectionProps<AllAuthorsData>)
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout withPadding={true} title={`${t('Discours')} :: ${t('All authors')}`}>
|
<PageLayout
|
||||||
|
withPadding={true}
|
||||||
|
title={`${t('Discours')} :: ${t('All authors')}`}
|
||||||
|
class={styles.allAuthorsPage}
|
||||||
|
desc="List of authors of the open editorial community"
|
||||||
|
>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<AllAuthors
|
<AllAuthors
|
||||||
isLoaded={Boolean(data()?.authors)}
|
isLoaded={Boolean(data()?.authors)}
|
||||||
authors={data()?.authors || []}
|
authors={data()?.authors || []}
|
||||||
topFollowedAuthors={data()?.topFollowedAuthors}
|
authorsByFollowers={data()?.authorsByFollowers}
|
||||||
topWritingAuthors={data()?.topShoutsAuthors}
|
authorsByShouts={data()?.authorsByShouts}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ReactionsProvider>
|
</ReactionsProvider>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useLocalize } from '~/context/localize'
|
||||||
import { ReactionsProvider } from '~/context/reactions'
|
import { ReactionsProvider } from '~/context/reactions'
|
||||||
import { loadShouts } from '~/graphql/api/public'
|
import { loadShouts } from '~/graphql/api/public'
|
||||||
import { Author, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
|
import { Author, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
|
||||||
|
import { getImageUrl } from '~/lib/getImageUrl'
|
||||||
import { SHOUTS_PER_PAGE } from '../../(home)'
|
import { SHOUTS_PER_PAGE } from '../../(home)'
|
||||||
|
|
||||||
const fetchAuthorShouts = async (slug: string, offset?: number) => {
|
const fetchAuthorShouts = async (slug: string, offset?: number) => {
|
||||||
|
@ -47,6 +48,12 @@ export default (props: RouteSectionProps<{ articles: Shout[] }>) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const cover = createMemo(() =>
|
||||||
|
author()?.pic
|
||||||
|
? getImageUrl(author()?.pic || '', { width: 1200 })
|
||||||
|
: getImageUrl('production/image/logo_image.png')
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary fallback={(_err) => <FourOuFourView />}>
|
<ErrorBoundary fallback={(_err) => <FourOuFourView />}>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
|
@ -54,8 +61,8 @@ export default (props: RouteSectionProps<{ articles: Shout[] }>) => {
|
||||||
title={`${t('Discours')} :: ${title()}`}
|
title={`${t('Discours')} :: ${title()}`}
|
||||||
headerTitle={author()?.name || ''}
|
headerTitle={author()?.name || ''}
|
||||||
slug={author()?.slug}
|
slug={author()?.slug}
|
||||||
articleBody={author()?.about || author()?.bio || ''}
|
desc={author()?.about || author()?.bio || ''}
|
||||||
cover={author()?.pic || ''}
|
cover={cover()}
|
||||||
>
|
>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<AuthorView
|
<AuthorView
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { Meta } from '@solidjs/meta'
|
|
||||||
import { useNavigate } from '@solidjs/router'
|
import { useNavigate } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createMemo } from 'solid-js'
|
|
||||||
import { AuthGuard } from '~/components/AuthGuard'
|
import { AuthGuard } from '~/components/AuthGuard'
|
||||||
import { Button } from '~/components/_shared/Button'
|
import { Button } from '~/components/_shared/Button'
|
||||||
import { Icon } from '~/components/_shared/Icon'
|
import { Icon } from '~/components/_shared/Icon'
|
||||||
|
@ -9,19 +7,11 @@ import { PageLayout } from '~/components/_shared/PageLayout'
|
||||||
import { useGraphQL } from '~/context/graphql'
|
import { useGraphQL } from '~/context/graphql'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import createShoutMutation from '~/graphql/mutation/core/article-create'
|
import createShoutMutation from '~/graphql/mutation/core/article-create'
|
||||||
import enKeywords from '~/intl/locales/en/keywords.json'
|
|
||||||
import ruKeywords from '~/intl/locales/ru/keywords.json'
|
|
||||||
import { getImageUrl } from '~/lib/getImageUrl'
|
|
||||||
import styles from '~/styles/Create.module.scss'
|
import styles from '~/styles/Create.module.scss'
|
||||||
import { LayoutType } from '~/types/common'
|
import { LayoutType } from '~/types/common'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { t, lang } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const ogImage = getImageUrl('production/image/logo_image.png')
|
|
||||||
const ogTitle = createMemo(() => t('Choose a post type'))
|
|
||||||
const description = createMemo(() =>
|
|
||||||
t('Participate in the Discours: share information, join the editorial team')
|
|
||||||
)
|
|
||||||
const client = useGraphQL()
|
const client = useGraphQL()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const handleCreate = async (layout: LayoutType) => {
|
const handleCreate = async (layout: LayoutType) => {
|
||||||
|
@ -32,17 +22,11 @@ export default () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<PageLayout title={`${t('Discours')} :: ${ogTitle()}`}>
|
<PageLayout
|
||||||
<Meta name="descprition" content={description()} />
|
title={`${t('Discours')} :: ${t('Choose a post type')}`}
|
||||||
<Meta name="keywords" content={lang() === 'ru' ? ruKeywords[''] : enKeywords['']} />
|
key="home"
|
||||||
<Meta name="og:type" content="article" />
|
desc="Participate in the Discours: share information, join the editorial team"
|
||||||
<Meta name="og:title" content={ogTitle()} />
|
>
|
||||||
<Meta name="og:image" content={ogImage} />
|
|
||||||
<Meta name="twitter: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()} />
|
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<article class={clsx('wide-container', 'container--static-page', styles.Create)}>
|
<article class={clsx('wide-container', 'container--static-page', styles.Create)}>
|
||||||
<h1>{t('Choose a post type')}</h1>
|
<h1>{t('Choose a post type')}</h1>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Params, RouteSectionProps, createAsync, useParams } from '@solidjs/router'
|
import { Params, RouteSectionProps, createAsync, useParams } from '@solidjs/router'
|
||||||
import { createEffect, createMemo, on } from 'solid-js'
|
import { createEffect, createMemo, on } from 'solid-js'
|
||||||
import { Topics } from '~/components/Nav/Topics'
|
import { TopicsNav } from '~/components/Nav/TopicsNav'
|
||||||
import { Expo } from '~/components/Views/Expo'
|
import { Expo } from '~/components/Views/Expo'
|
||||||
import { PageLayout } from '~/components/_shared/PageLayout'
|
import { PageLayout } from '~/components/_shared/PageLayout'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
|
@ -61,7 +61,7 @@ export default (props: RouteSectionProps<Shout[]>) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout withPadding={true} zeroBottomPadding={true} title={`${t('Discours')} :: ${title()}`}>
|
<PageLayout withPadding={true} zeroBottomPadding={true} title={`${t('Discours')} :: ${title()}`}>
|
||||||
<Topics />
|
<TopicsNav />
|
||||||
<Expo shouts={shouts() || []} layout={layout() as LayoutType} />
|
<Expo shouts={shouts() || []} layout={layout() as LayoutType} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -125,7 +125,12 @@ export default (props: RouteSectionProps<Shout[]>) => {
|
||||||
}
|
}
|
||||||
createEffect(() => setIsLoadMoreButtonVisible(offset() < (shouts()?.length || 0)))
|
createEffect(() => setIsLoadMoreButtonVisible(offset() < (shouts()?.length || 0)))
|
||||||
return (
|
return (
|
||||||
<PageLayout withPadding={true} title={`${t('Discours')} :: ${t('Feed')}`}>
|
<PageLayout
|
||||||
|
withPadding={true}
|
||||||
|
title={`${t('Discours')} :: ${t('Feed')}`}
|
||||||
|
key="feed"
|
||||||
|
desc="Independent media project about culture, science, art and society with horizontal editing"
|
||||||
|
>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<Feed shouts={shouts() || []} />
|
<Feed shouts={shouts() || []} />
|
||||||
</ReactionsProvider>
|
</ReactionsProvider>
|
||||||
|
|
|
@ -22,7 +22,12 @@ export default (props: RouteSectionProps<{ topics: Topic[] }>) => {
|
||||||
const { addTopics } = useTopics()
|
const { addTopics } = useTopics()
|
||||||
createEffect(() => addTopics(topics() || []))
|
createEffect(() => addTopics(topics() || []))
|
||||||
return (
|
return (
|
||||||
<PageLayout withPadding={true} title={`${t('Discours')} :: ${t('All topics')}`}>
|
<PageLayout
|
||||||
|
withPadding={true}
|
||||||
|
key="topics"
|
||||||
|
title={`${t('Discours')} :: ${t('All topics')}`}
|
||||||
|
desc="Thematic table of contents of the magazine. Here you can find all the topics that the community authors wrote about"
|
||||||
|
>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<AllTopics topics={topics() as Topic[]} />
|
<AllTopics topics={topics() as Topic[]} />
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { ReactionsProvider } from '~/context/reactions'
|
||||||
import { useTopics } from '~/context/topics'
|
import { useTopics } from '~/context/topics'
|
||||||
import { loadShouts } from '~/graphql/api/public'
|
import { loadShouts } from '~/graphql/api/public'
|
||||||
import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
|
import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||||
|
import { getImageUrl } from '~/lib/getImageUrl'
|
||||||
|
import { getArticleDescription } from '~/utils/meta'
|
||||||
import { SHOUTS_PER_PAGE } from '../(home)'
|
import { SHOUTS_PER_PAGE } from '../(home)'
|
||||||
|
|
||||||
const fetchTopicShouts = async (slug: string, offset?: number) => {
|
const fetchTopicShouts = async (slug: string, offset?: number) => {
|
||||||
|
@ -43,15 +45,26 @@ export default (props: RouteSectionProps<{ articles: Shout[] }>) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const desc = createMemo(() =>
|
||||||
|
topic()?.body
|
||||||
|
? getArticleDescription(topic()?.body || '')
|
||||||
|
: t('The most interesting publications on the topic', { topicName: title() })
|
||||||
|
)
|
||||||
|
const cover = createMemo(() =>
|
||||||
|
topic()?.pic
|
||||||
|
? getImageUrl(topic()?.pic || '', { width: 1200 })
|
||||||
|
: getImageUrl('production/image/logo_image.png')
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary fallback={(_err) => <FourOuFourView />}>
|
<ErrorBoundary fallback={(_err) => <FourOuFourView />}>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<PageLayout
|
<PageLayout
|
||||||
|
key="topic"
|
||||||
title={title()}
|
title={title()}
|
||||||
|
desc={desc()}
|
||||||
headerTitle={topic()?.title || ''}
|
headerTitle={topic()?.title || ''}
|
||||||
slug={topic()?.slug}
|
slug={topic()?.slug}
|
||||||
articleBody={topic()?.body || ''}
|
cover={cover()}
|
||||||
cover={topic()?.pic || ''}
|
|
||||||
>
|
>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<TopicView
|
<TopicView
|
||||||
|
|
Loading…
Reference in New Issue
Block a user