meta-refactored

This commit is contained in:
Untone 2024-07-09 12:13:13 +03:00
parent b204204a31
commit e3ac3cc406
31 changed files with 329 additions and 487 deletions

View File

@ -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}
/> />

View File

@ -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>
)
}

View File

@ -1 +0,0 @@
export { AuthorsList } from './AuthorsList'

View File

@ -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={
<> <>

View File

@ -1 +0,0 @@
export { Topics } from './Topics'

View File

@ -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 (

View File

@ -0,0 +1 @@
export { TopicsNav } from './TopicsNav'

View File

@ -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}>

View File

@ -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> </>
) )
} }

View File

@ -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> </>
) )
} }

View File

@ -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 />}>
<> <>

View File

@ -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 />

View File

@ -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}>

View File

@ -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>

View File

@ -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')}>

View File

@ -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 />

View File

@ -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(

View File

@ -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}`)

View File

@ -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"

View File

@ -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, Дискурс, статьи, журналистика, исследование"

View File

@ -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)} />

View File

@ -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)}
> >

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>
) )

View File

@ -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>

View File

@ -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[]} />

View File

@ -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