authors-all-fix+slug-404
This commit is contained in:
parent
d64f68579c
commit
f4f4e80816
|
@ -1,15 +1,16 @@
|
||||||
import { createPopper } from '@popperjs/core'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
// import { install } from 'ga-gtag'
|
// import { install } from 'ga-gtag'
|
||||||
|
import { createPopper } from '@popperjs/core'
|
||||||
|
import { Link, Meta } from '@solidjs/meta'
|
||||||
|
import { A, useSearchParams } from '@solidjs/router'
|
||||||
|
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'
|
||||||
import { isServer } from 'solid-js/web'
|
import { isServer } from 'solid-js/web'
|
||||||
|
import { useFeed } from '~/context/feed'
|
||||||
import { Link, Meta } from '@solidjs/meta'
|
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useReactions } from '~/context/reactions'
|
import { useReactions } from '~/context/reactions'
|
||||||
import { useSession } from '~/context/session'
|
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, 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, getOpenGraphImageUrl } from '~/lib/getImageUrl'
|
||||||
import { MediaItem } from '~/types/mediaitem'
|
import { MediaItem } from '~/types/mediaitem'
|
||||||
|
@ -34,8 +35,6 @@ import { CommentsTree } from './CommentsTree'
|
||||||
import { SharePopup, getShareUrl } from './SharePopup'
|
import { SharePopup, getShareUrl } from './SharePopup'
|
||||||
import { ShoutRatingControl } from './ShoutRatingControl'
|
import { ShoutRatingControl } from './ShoutRatingControl'
|
||||||
|
|
||||||
import { A, useSearchParams } from '@solidjs/router'
|
|
||||||
import { useFeed } from '~/context/feed'
|
|
||||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||||
import styles from './Article.module.scss'
|
import styles from './Article.module.scss'
|
||||||
|
|
||||||
|
@ -79,24 +78,24 @@ export const FullArticle = (props: Props) => {
|
||||||
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
|
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
|
||||||
const { addSeen } = useFeed()
|
const { addSeen } = useFeed()
|
||||||
|
|
||||||
const formattedDate = createMemo(() => formatDate(new Date((props.article?.published_at || 0) * 1000)))
|
const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000)))
|
||||||
|
|
||||||
const canEdit = createMemo(
|
const canEdit = createMemo(
|
||||||
() =>
|
() =>
|
||||||
Boolean(author()?.id) &&
|
Boolean(author()?.id) &&
|
||||||
(props.article?.authors?.some((a) => Boolean(a) && a?.id === author().id) ||
|
(props.article.authors?.some((a) => Boolean(a) && a?.id === author().id) ||
|
||||||
props.article?.created_by?.id === author().id ||
|
props.article.created_by?.id === author().id ||
|
||||||
session()?.user?.roles?.includes('editor'))
|
session()?.user?.roles?.includes('editor'))
|
||||||
)
|
)
|
||||||
|
|
||||||
const mainTopic = createMemo(() => {
|
const mainTopic = createMemo(() => {
|
||||||
const mainTopicSlug = (props.article?.topics?.length || 0) > 0 ? props.article.main_topic : null
|
const mainTopicSlug = (props.article.topics?.length || 0) > 0 ? props.article.main_topic : null
|
||||||
const mt = props.article?.topics?.find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug)
|
const mt = props.article.topics?.find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug)
|
||||||
if (mt) {
|
if (mt) {
|
||||||
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title
|
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title
|
||||||
return mt
|
return mt
|
||||||
}
|
}
|
||||||
return props.article?.topics?.[0]
|
return props.article.topics?.[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleBookmarkButtonClick = (ev: MouseEvent | undefined) => {
|
const handleBookmarkButtonClick = (ev: MouseEvent | undefined) => {
|
||||||
|
@ -107,10 +106,10 @@ export const FullArticle = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = createMemo(() => {
|
const body = createMemo(() => {
|
||||||
if (props.article?.layout === 'literature') {
|
if (props.article.layout === 'literature') {
|
||||||
try {
|
try {
|
||||||
if (props.article?.media) {
|
if (props.article.media) {
|
||||||
const media = JSON.parse(props.article?.media)
|
const media = JSON.parse(props.article.media)
|
||||||
if (media.length > 0) {
|
if (media.length > 0) {
|
||||||
return media[0].body
|
return media[0].body
|
||||||
}
|
}
|
||||||
|
@ -119,7 +118,7 @@ export const FullArticle = (props: Props) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return props.article?.body || ''
|
return props.article.body || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageUrls = createMemo(() => {
|
const imageUrls = createMemo(() => {
|
||||||
|
@ -145,7 +144,7 @@ export const FullArticle = (props: Props) => {
|
||||||
|
|
||||||
const media = createMemo<MediaItem[]>(() => {
|
const media = createMemo<MediaItem[]>(() => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(props.article?.media || '[]')
|
return JSON.parse(props.article.media || '[]')
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -304,7 +303,8 @@ export const FullArticle = (props: Props) => {
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// install('G-LQ4B87H8C2')
|
// install('G-LQ4B87H8C2')
|
||||||
await loadReactionsBy({ by: { shout: props.article.slug } })
|
const opts: QueryLoad_Reactions_ByArgs = { by: { shout: props.article.slug }, limit: 999, offset: 0 }
|
||||||
|
const _rrr = await loadReactionsBy(opts)
|
||||||
addSeen(props.article.slug)
|
addSeen(props.article.slug)
|
||||||
setIsReactionsLoaded(true)
|
setIsReactionsLoaded(true)
|
||||||
document.title = props.article.title
|
document.title = props.article.title
|
||||||
|
@ -326,18 +326,18 @@ export const FullArticle = (props: Props) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const cover = props.article.cover ?? 'production/image/logo_image.png'
|
const cover = props.article.cover || 'production/image/logo_image.png'
|
||||||
const ogImage = getOpenGraphImageUrl(cover, {
|
const ogImage = getOpenGraphImageUrl(cover, {
|
||||||
title: props.article.title,
|
title: props.article.title,
|
||||||
topic: mainTopic()?.title || '',
|
topic: mainTopic()?.title || '',
|
||||||
author: props.article?.authors?.[0]?.name || '',
|
author: props.article.authors?.[0]?.name || '',
|
||||||
width: 1200
|
width: 1200
|
||||||
})
|
})
|
||||||
|
|
||||||
const description = getArticleDescription(props.article.description || body() || media()[0]?.body)
|
const description = getArticleDescription(props.article.description || body() || media()[0]?.body)
|
||||||
const ogTitle = props.article.title
|
const ogTitle = props.article.title
|
||||||
const keywords = getArticleKeywords(props.article)
|
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
|
return lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name
|
||||||
}
|
}
|
||||||
|
@ -363,19 +363,19 @@ export const FullArticle = (props: Props) => {
|
||||||
onClick={handleArticleBodyClick}
|
onClick={handleArticleBodyClick}
|
||||||
>
|
>
|
||||||
{/*TODO: Check styles.shoutTopic*/}
|
{/*TODO: Check styles.shoutTopic*/}
|
||||||
<Show when={props.article?.layout !== 'audio'}>
|
<Show when={props.article.layout !== 'audio'}>
|
||||||
<div class={styles.shoutHeader}>
|
<div class={styles.shoutHeader}>
|
||||||
<Show when={mainTopic()}>
|
<Show when={mainTopic()}>
|
||||||
<CardTopic title={mainTopic()?.title || ''} slug={mainTopic()?.slug || ''} />
|
<CardTopic title={mainTopic()?.title || ''} slug={mainTopic()?.slug || ''} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<h1>{props.article?.title || ''}</h1>
|
<h1>{props.article.title || ''}</h1>
|
||||||
<Show when={props.article?.subtitle}>
|
<Show when={props.article.subtitle}>
|
||||||
<h4>{props.article?.subtitle || ''}</h4>
|
<h4>{props.article.subtitle || ''}</h4>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class={styles.shoutAuthor}>
|
<div class={styles.shoutAuthor}>
|
||||||
<For each={props.article?.authors}>
|
<For each={props.article.authors}>
|
||||||
{(a: Maybe<Author>, index: () => number) => (
|
{(a: Maybe<Author>, index: () => number) => (
|
||||||
<>
|
<>
|
||||||
<Show when={index() > 0}>, </Show>
|
<Show when={index() > 0}>, </Show>
|
||||||
|
@ -386,39 +386,39 @@ export const FullArticle = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
props.article?.cover &&
|
props.article.cover &&
|
||||||
props.article?.layout !== 'video' &&
|
props.article.layout !== 'video' &&
|
||||||
props.article?.layout !== 'image'
|
props.article.layout !== 'image'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<figure class="img-align-column">
|
<figure class="img-align-column">
|
||||||
<Image
|
<Image
|
||||||
width={800}
|
width={800}
|
||||||
alt={props.article?.cover_caption || ''}
|
alt={props.article.cover_caption || ''}
|
||||||
src={props.article?.cover || ''}
|
src={props.article.cover || ''}
|
||||||
/>
|
/>
|
||||||
<figcaption innerHTML={props.article?.cover_caption || ''} />
|
<figcaption innerHTML={props.article.cover_caption || ''} />
|
||||||
</figure>
|
</figure>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.article?.lead}>
|
<Show when={props.article.lead}>
|
||||||
<section class={styles.lead} innerHTML={props.article?.lead || ''} />
|
<section class={styles.lead} innerHTML={props.article.lead || ''} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.article?.layout === 'audio'}>
|
<Show when={props.article.layout === 'audio'}>
|
||||||
<AudioHeader
|
<AudioHeader
|
||||||
title={props.article?.title || ''}
|
title={props.article.title || ''}
|
||||||
cover={props.article?.cover || ''}
|
cover={props.article.cover || ''}
|
||||||
artistData={media()?.[0]}
|
artistData={media()?.[0]}
|
||||||
topic={mainTopic() as Topic}
|
topic={mainTopic() as Topic}
|
||||||
/>
|
/>
|
||||||
<Show when={media().length > 0}>
|
<Show when={media().length > 0}>
|
||||||
<div class="media-items">
|
<div class="media-items">
|
||||||
<AudioPlayer media={media()} articleSlug={props.article?.slug || ''} body={body()} />
|
<AudioPlayer media={media()} articleSlug={props.article.slug || ''} body={body()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={media() && props.article?.layout === 'video'}>
|
<Show when={media() && props.article.layout === 'video'}>
|
||||||
<div class="media-items">
|
<div class="media-items">
|
||||||
<For each={media() || []}>
|
<For each={media() || []}>
|
||||||
{(m: MediaItem) => (
|
{(m: MediaItem) => (
|
||||||
|
@ -542,7 +542,7 @@ export const FullArticle = (props: Props) => {
|
||||||
<Popover content={t('Edit')}>
|
<Popover content={t('Edit')}>
|
||||||
{(triggerRef: (el: HTMLElement) => void) => (
|
{(triggerRef: (el: HTMLElement) => void) => (
|
||||||
<div class={styles.shoutStatsItem} ref={triggerRef}>
|
<div class={styles.shoutStatsItem} ref={triggerRef}>
|
||||||
<A href={`/edit/${props.article?.id}`} class={styles.shoutStatsItemInner}>
|
<A href={`/edit/${props.article.id}`} class={styles.shoutStatsItemInner}>
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
|
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
</A>
|
</A>
|
||||||
|
@ -577,9 +577,9 @@ export const FullArticle = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.article?.topics?.length}>
|
<Show when={props.article.topics?.length}>
|
||||||
<div class={styles.topicsList}>
|
<div class={styles.topicsList}>
|
||||||
<For each={props.article?.topics || []}>
|
<For each={props.article.topics || []}>
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<div class={styles.shoutTopic}>
|
<div class={styles.shoutTopic}>
|
||||||
<A href={`/topic/${topic?.slug || ''}`}>
|
<A href={`/topic/${topic?.slug || ''}`}>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, Show, createEffect, createSignal, on } from 'solid-js'
|
import { For, Show, createEffect, createSignal, on } from 'solid-js'
|
||||||
import { useAuthors } from '~/context/authors'
|
import { useAuthors } from '~/context/authors'
|
||||||
import { useGraphQL } from '~/context/graphql'
|
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import loadAuthorsByQuery from '~/graphql/query/core/authors-load-by'
|
import { loadAuthors } from '~/graphql/api/public'
|
||||||
import { Author } from '~/graphql/schema/core.gen'
|
import { Author } from '~/graphql/schema/core.gen'
|
||||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||||
import { InlineLoader } from '../InlineLoader'
|
import { InlineLoader } from '../InlineLoader'
|
||||||
|
import { AUTHORS_PER_PAGE } from '../Views/AllAuthors/AllAuthors'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
import styles from './AuthorsList.module.scss'
|
import styles from './AuthorsList.module.scss'
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ type Props = {
|
||||||
allAuthorsLength?: number
|
allAuthorsLength?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
// pagination handling, loadAuthors cached from api, addAuthors to context
|
||||||
|
|
||||||
export const AuthorsList = (props: Props) => {
|
export const AuthorsList = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
@ -27,18 +27,17 @@ export const AuthorsList = (props: Props) => {
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
|
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
|
||||||
const [allLoaded, setAllLoaded] = createSignal(false)
|
const [allLoaded, setAllLoaded] = createSignal(false)
|
||||||
const { query } = useGraphQL()
|
|
||||||
|
|
||||||
const fetchAuthors = async (queryType: Props['query'], page: number) => {
|
const fetchAuthors = async (queryType: Props['query'], page: number) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const offset = PAGE_SIZE * page
|
const offset = AUTHORS_PER_PAGE * page
|
||||||
const resp = await query(loadAuthorsByQuery, {
|
const fetcher = await loadAuthors({
|
||||||
by: { order: queryType },
|
by: { order: queryType },
|
||||||
limit: PAGE_SIZE,
|
limit: AUTHORS_PER_PAGE,
|
||||||
offset
|
offset
|
||||||
})
|
})
|
||||||
const result = resp?.data?.load_authors_by
|
const result = await fetcher()
|
||||||
if ((result?.length || 0) > 0) {
|
if (result) {
|
||||||
addAuthors([...result])
|
addAuthors([...result])
|
||||||
if (queryType === 'shouts') {
|
if (queryType === 'shouts') {
|
||||||
setAuthorsByShouts((prev) => [...(prev || []), ...result])
|
setAuthorsByShouts((prev) => [...(prev || []), ...result])
|
||||||
|
@ -70,17 +69,7 @@ export const AuthorsList = (props: Props) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const authorsList = () => (props.query === 'shouts' ? authorsByShouts() : authorsByFollowers())
|
const authorsList = () => (props.query === 'shouts' ? authorsByShouts() : authorsByFollowers())
|
||||||
|
createEffect(() => setAllLoaded(props.allAuthorsLength === authorsList.length))
|
||||||
// TODO: do it with backend
|
|
||||||
// createEffect(() => {
|
|
||||||
// if (props.searchQuery) {
|
|
||||||
// // search logic
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setAllLoaded(props.allAuthorsLength === authorsList.length)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.AuthorsList, props.class)}>
|
<div class={clsx(styles.AuthorsList, props.class)}>
|
||||||
|
|
|
@ -8,9 +8,8 @@ import { composeMediaItems } from '~/utils/composeMediaItems'
|
||||||
import { AudioPlayer } from '../../Article/AudioPlayer'
|
import { AudioPlayer } from '../../Article/AudioPlayer'
|
||||||
import styles from './AudioUploader.module.scss'
|
import styles from './AudioUploader.module.scss'
|
||||||
|
|
||||||
|
|
||||||
if (!isServer && window) window.Buffer = Buffer
|
if (!isServer && window) window.Buffer = Buffer
|
||||||
console.debug('buffer patch passed')
|
// console.debug('buffer patch passed')
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
class?: string
|
class?: string
|
||||||
|
|
|
@ -220,7 +220,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
|
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<A href={`/article${props.article?.slug || ''}`}>
|
<A href={`/${props.article?.slug || ''}`}>
|
||||||
<div class={styles.shoutCardTitle}>
|
<div class={styles.shoutCardTitle}>
|
||||||
<span class={styles.shoutCardLinkWrapper}>
|
<span class={styles.shoutCardLinkWrapper}>
|
||||||
<span class={styles.shoutCardLinkContainer} innerHTML={title} />
|
<span class={styles.shoutCardLinkContainer} innerHTML={title} />
|
||||||
|
|
|
@ -191,7 +191,7 @@ export const Header = (props: Props) => {
|
||||||
<div class={styles.articleHeader}>{props.title}</div>
|
<div class={styles.articleHeader}>{props.title}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={clsx(styles.mainNavigation, { [styles.fixed]: fixed() })}>
|
<div class={clsx(styles.mainNavigation, { [styles.fixed]: fixed() })}>
|
||||||
<ul class="view-switcher" onClick={() => !fixed() && toggleFixed()}>
|
<ul class="view-switcher">
|
||||||
<Link
|
<Link
|
||||||
onMouseOver={() => toggleSubnavigation(true, setIsZineVisible)}
|
onMouseOver={() => toggleSubnavigation(true, setIsZineVisible)}
|
||||||
onMouseOut={hideSubnavigation}
|
onMouseOut={hideSubnavigation}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { Author } from '~/graphql/schema/core.gen'
|
||||||
import enKeywords from '~/intl/locales/en/keywords.json'
|
import enKeywords from '~/intl/locales/en/keywords.json'
|
||||||
import ruKeywords from '~/intl/locales/ru/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 { getImageUrl } from '~/lib/getImageUrl'
|
import { getImageUrl } from '~/lib/getImageUrl'
|
||||||
import { scrollHandler } from '~/utils/scroll'
|
import { scrollHandler } from '~/utils/scroll'
|
||||||
import { AuthorsList } from '../../AuthorsList'
|
import { AuthorsList } from '../../AuthorsList'
|
||||||
|
@ -27,23 +28,29 @@ export const ABC = {
|
||||||
en: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'
|
en: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// useAuthors sorted from context, set filter/sort
|
||||||
|
|
||||||
export const AllAuthors = (props: Props) => {
|
export const AllAuthors = (props: Props) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const [searchQuery, setSearchQuery] = createSignal('')
|
|
||||||
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, addAuthors, setAuthorsSort } = useAuthors()
|
const { authorsSorted, setAuthorsSort } = useAuthors()
|
||||||
|
const authors = createMemo(() => props.authors || authorsSorted())
|
||||||
|
|
||||||
|
// filter
|
||||||
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
|
const [filteredAuthors, setFilteredAuthors] = createSignal<Author[]>([])
|
||||||
|
createEffect(() =>
|
||||||
|
authors() && setFilteredAuthors((_prev: Author[]) => dummyFilter(authors(), searchQuery(), lang()) as Author[])
|
||||||
|
)
|
||||||
|
|
||||||
|
// sort by
|
||||||
onMount(() => !searchParams?.by && changeSearchParams({ by: 'name' }))
|
onMount(() => !searchParams?.by && changeSearchParams({ by: 'name' }))
|
||||||
createEffect(on(() => searchParams?.by || 'name', setAuthorsSort || ((_) => null), {}))
|
createEffect(on(() => searchParams?.by || 'name', setAuthorsSort || ((_) => null), {}))
|
||||||
createEffect(on(() => props.authors || [], addAuthors || ((_) => null), {}))
|
|
||||||
|
|
||||||
const filteredAuthors = createMemo(() => {
|
|
||||||
const query = searchQuery().toLowerCase()
|
|
||||||
return authorsSorted?.()?.filter((a: Author) => a?.name?.toLowerCase().includes(query)) || []
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// store by first char
|
||||||
const byLetterFiltered = createMemo<{ [letter: string]: Author[] }>(() => {
|
const byLetterFiltered = createMemo<{ [letter: string]: Author[] }>(() => {
|
||||||
|
console.debug('[components.AllAuthors] byLetterFiltered')
|
||||||
return (
|
return (
|
||||||
filteredAuthors()?.reduce(
|
filteredAuthors()?.reduce(
|
||||||
(acc, author: Author) => authorLetterReduce(acc, author, lang()),
|
(acc, author: Author) => authorLetterReduce(acc, author, lang()),
|
||||||
|
@ -65,7 +72,7 @@ export const AllAuthors = (props: Props) => {
|
||||||
const description = createMemo(() => t('List of authors of the open editorial community'))
|
const description = createMemo(() => t('List of authors of the open editorial community'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.allAuthorsPage, 'wide-container')}>
|
<div class={clsx([styles.allAuthorsPage, 'wide-container'])}>
|
||||||
<Meta name="descprition" content={description() || ''} />
|
<Meta name="descprition" content={description() || ''} />
|
||||||
<Meta name="keywords" content={lang() === 'ru' ? ruKeywords[''] : enKeywords['']} />
|
<Meta name="keywords" content={lang() === 'ru' ? ruKeywords[''] : enKeywords['']} />
|
||||||
<Meta name="og:type" content="article" />
|
<Meta name="og:type" content="article" />
|
||||||
|
@ -165,9 +172,9 @@ export const AllAuthors = (props: Props) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={searchParams?.by !== 'name' && props.isLoaded}>
|
<Show when={authors().length && searchParams?.by !== 'name' && props.isLoaded}>
|
||||||
<AuthorsList
|
<AuthorsList
|
||||||
allAuthorsLength={authorsSorted?.()?.length || 0}
|
allAuthorsLength={authors().length}
|
||||||
searchQuery={searchQuery()}
|
searchQuery={searchQuery()}
|
||||||
query={searchParams?.by === 'followers' ? 'followers' : 'shouts'}
|
query={searchParams?.by === 'followers' ? 'followers' : 'shouts'}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -173,13 +173,13 @@ export const AuthorsProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextValue: AuthorsContextType = {
|
const contextValue: AuthorsContextType = {
|
||||||
loadAllAuthors,
|
|
||||||
authorsEntities,
|
authorsEntities,
|
||||||
authorsSorted,
|
authorsSorted,
|
||||||
addAuthors,
|
addAuthors,
|
||||||
addAuthor,
|
addAuthor,
|
||||||
loadAuthor,
|
loadAuthor,
|
||||||
loadAuthors: loadAuthorsPaginated,
|
loadAuthors: loadAuthorsPaginated, // with stat
|
||||||
|
loadAllAuthors, // without stat
|
||||||
topAuthors,
|
topAuthors,
|
||||||
authorsByTopic,
|
authorsByTopic,
|
||||||
setAuthorsSort
|
setAuthorsSort
|
||||||
|
|
|
@ -2,10 +2,10 @@ import type { JSX } from 'solid-js'
|
||||||
|
|
||||||
import { createContext, onCleanup, useContext } from 'solid-js'
|
import { createContext, onCleanup, useContext } from 'solid-js'
|
||||||
import { createStore, reconcile } from 'solid-js/store'
|
import { createStore, reconcile } from 'solid-js/store'
|
||||||
|
import { loadReactions } from '~/graphql/api/public'
|
||||||
import createReactionMutation from '~/graphql/mutation/core/reaction-create'
|
import createReactionMutation from '~/graphql/mutation/core/reaction-create'
|
||||||
import destroyReactionMutation from '~/graphql/mutation/core/reaction-destroy'
|
import destroyReactionMutation from '~/graphql/mutation/core/reaction-destroy'
|
||||||
import updateReactionMutation from '~/graphql/mutation/core/reaction-update'
|
import updateReactionMutation from '~/graphql/mutation/core/reaction-update'
|
||||||
import getReactionsByQuery from '~/graphql/query/core/reactions-load-by'
|
|
||||||
import {
|
import {
|
||||||
MutationCreate_ReactionArgs,
|
MutationCreate_ReactionArgs,
|
||||||
MutationUpdate_ReactionArgs,
|
MutationUpdate_ReactionArgs,
|
||||||
|
@ -37,11 +37,11 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
const [reactionsByShout, setReactionsByShout] = createStore<Record<number, Reaction[]>>({})
|
const [reactionsByShout, setReactionsByShout] = createStore<Record<number, Reaction[]>>({})
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const { query, mutation } = useGraphQL()
|
const { mutation } = useGraphQL()
|
||||||
|
|
||||||
const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise<Reaction[]> => {
|
const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise<Reaction[]> => {
|
||||||
const resp = await query(getReactionsByQuery, opts)
|
const fetcher = await loadReactions({ ...opts })
|
||||||
const result = resp?.data?.load_reactions_by || []
|
const result = (await fetcher()) || []
|
||||||
const newReactionsByShout: Record<string, Reaction[]> = {}
|
const newReactionsByShout: Record<string, Reaction[]> = {}
|
||||||
const newReactionEntities = result.reduce(
|
const newReactionEntities = result.reduce(
|
||||||
(acc: { [reaction_id: number]: Reaction }, reaction: Reaction) => {
|
(acc: { [reaction_id: number]: Reaction }, reaction: Reaction) => {
|
||||||
|
|
|
@ -58,6 +58,7 @@ export const loadShouts = (options: LoadShoutsOptions) => {
|
||||||
export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => {
|
export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => {
|
||||||
const page = `${options.offset || 0}-${(options?.limit || 0) + (options.offset || 0)}`
|
const page = `${options.offset || 0}-${(options?.limit || 0) + (options.offset || 0)}`
|
||||||
const filter = new URLSearchParams(options.by as Record<string, string>)
|
const filter = new URLSearchParams(options.by as Record<string, string>)
|
||||||
|
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
|
||||||
|
@ -66,6 +67,7 @@ export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getShout = (options: QueryGet_ShoutArgs) => {
|
export const getShout = (options: QueryGet_ShoutArgs) => {
|
||||||
|
// console.debug('[lib.api] get shout cached fetcher returned', defaultClient)
|
||||||
return cache(
|
return cache(
|
||||||
async () => {
|
async () => {
|
||||||
const resp = await defaultClient.query(loadReactionsByQuery, { ...options }).toPromise()
|
const resp = await defaultClient.query(loadReactionsByQuery, { ...options }).toPromise()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query {
|
query GetAuthorsAllQuery {
|
||||||
get_authors_all {
|
get_authors_all {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query AuthorsAllQuery($by: AuthorsBy!, $limit: Int, $offset: Int) {
|
query LoadAuthorsBy($by: AuthorsBy!, $limit: Int, $offset: Int) {
|
||||||
get_authors(by: $by, limit: $limit, offset: $offset) {
|
load_authors_by(by: $by, limit: $limit, offset: $offset) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { HttpStatusCode } from '@solidjs/start'
|
import { HttpStatusCode } from '@solidjs/start'
|
||||||
|
import { onMount } from 'solid-js'
|
||||||
import { FourOuFourView } from '../components/Views/FourOuFour'
|
import { FourOuFourView } from '../components/Views/FourOuFour'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../context/localize'
|
import { useLocalize } from '../context/localize'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
onMount(() => console.info('404 page'))
|
||||||
return (
|
return (
|
||||||
<PageLayout isHeaderFixed={false} hideFooter={true} title={t('Nothing is here')}>
|
<PageLayout isHeaderFixed={false} hideFooter={true} title={t('Nothing is here')}>
|
||||||
<FourOuFourView />
|
<FourOuFourView />
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { RouteSectionProps, createAsync, useLocation, useParams } from '@solidjs/router'
|
import { RouteDefinition, RouteSectionProps, createAsync, redirect, useLocation, useParams } from '@solidjs/router'
|
||||||
import { ErrorBoundary, Suspense, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
import { HttpStatusCode } from '@solidjs/start'
|
||||||
import { FourOuFourView } from '~/components/Views/FourOuFour'
|
import { ErrorBoundary, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||||
import { Loading } from '~/components/_shared/Loading'
|
import { Loading } from '~/components/_shared/Loading'
|
||||||
import { gaIdentity } from '~/config'
|
import { gaIdentity } from '~/config'
|
||||||
|
import { useFeed } from '~/context/feed'
|
||||||
import { useLocalize } from '~/context/localize'
|
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'
|
||||||
|
@ -11,71 +12,83 @@ 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'
|
||||||
|
|
||||||
const fetchShout = async (slug: string) => {
|
|
||||||
|
const fetchShout = async (slug: string): Promise<Shout> => {
|
||||||
const shoutLoader = getShout({ slug })
|
const shoutLoader = getShout({ slug })
|
||||||
return await shoutLoader()
|
const shout = await shoutLoader()
|
||||||
|
if (!shout) {
|
||||||
|
throw new Error('Shout not found')
|
||||||
|
}
|
||||||
|
return shout
|
||||||
}
|
}
|
||||||
|
|
||||||
export const route = {
|
|
||||||
load: async ({ params }: RouteSectionProps<{ article: Shout }>) => await fetchShout(params.slug)
|
export const route: RouteDefinition = {
|
||||||
|
load: async ({ params }) => {
|
||||||
|
try {
|
||||||
|
return await fetchShout(params.slug)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading shout:', error)
|
||||||
|
throw new Response(null, {
|
||||||
|
status: 404,
|
||||||
|
statusText: 'Not Found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: RouteSectionProps<{ article: Shout }>) => {
|
export default (props: RouteSectionProps<{ article: Shout }>) => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const loc = useLocation()
|
const loc = useLocation()
|
||||||
const article = createAsync(async () => props.data.article || (await fetchShout(params.slug)))
|
const { articleEntities } = useFeed()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const [scrollToComments, setScrollToComments] = createSignal<boolean>(false)
|
const [scrollToComments, setScrollToComments] = createSignal<boolean>(false)
|
||||||
const title = createMemo(
|
|
||||||
() => `${article()?.authors?.[0]?.name || t('Discours')} :: ${article()?.title || ''}`
|
const article = createAsync(async () => {
|
||||||
)
|
if (params.slug && articleEntities?.()) {
|
||||||
|
return articleEntities()?.[params.slug] || props.data.article || await fetchShout(params.slug)
|
||||||
|
}
|
||||||
|
throw redirect('/404', { status: 404 })
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = createMemo(() => `${article()?.authors?.[0]?.name || t('Discours')} :: ${article()?.title || ''}`)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (gaIdentity) {
|
if (gaIdentity && article()?.id) {
|
||||||
try {
|
try {
|
||||||
console.info('[routes.slug] mounted, connecting ga...')
|
|
||||||
await loadGAScript(gaIdentity)
|
await loadGAScript(gaIdentity)
|
||||||
initGA(gaIdentity)
|
initGA(gaIdentity)
|
||||||
console.debug('[routes.slug] Google Analytics connected successfully')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[routes.slug] Failed to connect Google Analytics:', error)
|
console.warn('Failed to connect Google Analytics:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(on(article, (a?: Shout) => {
|
||||||
on(
|
if (!a) return
|
||||||
article,
|
window?.gtag?.('event', 'page_view', {
|
||||||
(a?: Shout) => {
|
page_title: a.title,
|
||||||
if (!a) return
|
page_location: window?.location.href || '',
|
||||||
console.debug('[routes.slug] article found')
|
page_path: loc.pathname
|
||||||
window?.gtag?.('event', 'page_view', {
|
})
|
||||||
page_title: a.title,
|
}, { defer: true }))
|
||||||
page_location: window?.location.href || '',
|
|
||||||
page_path: loc.pathname
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ defer: true }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary fallback={(_err) => <FourOuFourView />}>
|
<ErrorBoundary fallback={() => <HttpStatusCode code={404} />}>
|
||||||
<Suspense fallback={<Loading />}>
|
<Show when={article()?.id} fallback={<Loading />}>
|
||||||
<PageLayout
|
<PageLayout
|
||||||
title={title()}
|
title={title()}
|
||||||
headerTitle={article()?.title || ''}
|
headerTitle={article()?.title || ''}
|
||||||
slug={article()?.slug}
|
slug={article()?.slug}
|
||||||
articleBody={article()?.body}
|
articleBody={article()?.body}
|
||||||
cover={article()?.cover || ''}
|
cover={article()?.cover || ''}
|
||||||
scrollToComments={(value) => {
|
scrollToComments={(value) => setScrollToComments(value)}
|
||||||
setScrollToComments(value)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<FullArticle article={article() as Shout} scrollToComments={scrollToComments()} />
|
<FullArticle article={article() as Shout} scrollToComments={scrollToComments()} />
|
||||||
</ReactionsProvider>
|
</ReactionsProvider>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</Suspense>
|
</Show>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { RouteDefinition, RouteLoadFuncArgs, type RouteSectionProps, createAsync } from '@solidjs/router'
|
import { RouteDefinition, RouteLoadFuncArgs, type RouteSectionProps, createAsync } from '@solidjs/router'
|
||||||
import { Suspense, createReaction } 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 { Loading } from '~/components/_shared/Loading'
|
import { Loading } from '~/components/_shared/Loading'
|
||||||
|
@ -22,22 +22,56 @@ const fetchAllAuthors = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const route = {
|
export const route = {
|
||||||
load: ({ location: { query } }: RouteLoadFuncArgs) =>
|
load: async ({ location: { query } }: RouteLoadFuncArgs) => {
|
||||||
fetchAuthorsWithStat(Number.parseInt(query.offset), query.by || 'name')
|
const by = query.by
|
||||||
|
const isAll = !by || by === 'name'
|
||||||
|
return {
|
||||||
|
authors: isAll && await fetchAllAuthors(),
|
||||||
|
topFollowedAuthors: await fetchAuthorsWithStat(10, 'followers'),
|
||||||
|
topShoutsAuthors: await fetchAuthorsWithStat(10, 'shouts')
|
||||||
|
} as AllAuthorsData
|
||||||
|
}
|
||||||
} satisfies RouteDefinition
|
} satisfies RouteDefinition
|
||||||
|
|
||||||
export default function AllAuthorsPage(props: RouteSectionProps<{ authors: Author[] }>) {
|
type AllAuthorsData = { authors: Author[], topFollowedAuthors: Author[], topShoutsAuthors: Author[] }
|
||||||
|
|
||||||
|
// addAuthors to context
|
||||||
|
|
||||||
|
export default function AllAuthorsPage(props: RouteSectionProps<AllAuthorsData>) {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { authorsSorted, addAuthors } = useAuthors()
|
const { addAuthors } = useAuthors()
|
||||||
const authors = createAsync<Author[]>(
|
|
||||||
async () => authorsSorted?.() || props.data.authors || (await fetchAllAuthors())
|
// async load data: from ssr or fetch
|
||||||
)
|
const data = createAsync<AllAuthorsData>(async () => {
|
||||||
createReaction(() => typeof addAuthors === 'function' && addAuthors?.(authors() || []))
|
if (props.data) return props.data
|
||||||
|
return {
|
||||||
|
authors: await fetchAllAuthors(),
|
||||||
|
topFollowedAuthors: await fetchAuthorsWithStat(10, 'followers'),
|
||||||
|
topShoutsAuthors: await fetchAuthorsWithStat(10, 'shouts')
|
||||||
|
} as AllAuthorsData
|
||||||
|
})
|
||||||
|
|
||||||
|
// update context when data is loaded
|
||||||
|
createEffect(on([data, () => addAuthors],
|
||||||
|
([data, aa])=> {
|
||||||
|
if(data && aa) {
|
||||||
|
aa(data.authors as Author[])
|
||||||
|
aa(data.topFollowedAuthors as Author[])
|
||||||
|
aa(data.topShoutsAuthors as Author[])
|
||||||
|
console.debug('[routes.author] added all authors:', data.authors)
|
||||||
|
}
|
||||||
|
}, { defer: true}
|
||||||
|
))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout withPadding={true} title={`${t('Discours')} :: ${t('All authors')}`}>
|
<PageLayout withPadding={true} title={`${t('Discours')} :: ${t('All authors')}`}>
|
||||||
<ReactionsProvider>
|
<ReactionsProvider>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<AllAuthors authors={authors() || []} isLoaded={Boolean(authors())} />
|
<AllAuthors
|
||||||
|
isLoaded={Boolean(data()?.authors)}
|
||||||
|
authors={data()?.authors || []}
|
||||||
|
topFollowedAuthors={data()?.topFollowedAuthors}
|
||||||
|
topWritingAuthors={data()?.topShoutsAuthors}/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ReactionsProvider>
|
</ReactionsProvider>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user