authors-all-fix+slug-404

This commit is contained in:
Untone 2024-07-07 16:48:53 +03:00
parent d64f68579c
commit f4f4e80816
14 changed files with 179 additions and 134 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(
article,
(a?: Shout) => {
if (!a) return if (!a) return
console.debug('[routes.slug] article found')
window?.gtag?.('event', 'page_view', { window?.gtag?.('event', 'page_view', {
page_title: a.title, page_title: a.title,
page_location: window?.location.href || '', page_location: window?.location.href || '',
page_path: loc.pathname page_path: loc.pathname
}) })
}, }, { defer: true }))
{ 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>
) )
} }

View File

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