topic navigation fix (#346)

* topic navigation fix

* topic loading state
This commit is contained in:
Igor Lobanov 2023-12-21 13:48:13 +01:00 committed by GitHub
parent d463c83fe3
commit 87b6e44eb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 161 deletions

View File

@ -2,7 +2,7 @@ import type { Shout, Topic } from '../../graphql/types.gen'
import { Meta } from '@solidjs/meta' import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createMemo, onMount, createSignal, createEffect } from 'solid-js' import { For, Show, createMemo, onMount, createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
@ -14,7 +14,6 @@ import { getImageUrl } from '../../utils/getImageUrl'
import { getDescription } from '../../utils/meta' import { getDescription } 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 { Loading } from '../_shared/Loading'
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper' import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
import { Beside } from '../Feed/Beside' import { Beside } from '../Feed/Beside'
import { Row1 } from '../Feed/Row1' import { Row1 } from '../Feed/Row1'
@ -32,8 +31,6 @@ interface Props {
topic: Topic topic: Topic
shouts: Shout[] shouts: Shout[]
topicSlug: string topicSlug: string
isLoaded: boolean
title: (val: string) => string
} }
export const PRERENDERED_ARTICLES_COUNT = 28 export const PRERENDERED_ARTICLES_COUNT = 28
@ -88,7 +85,6 @@ export const TopicView = (props: Props) => {
) )
const pageTitle = `#${capitalize(topic().title, true)}` const pageTitle = `#${capitalize(topic().title, true)}`
createEffect(() => props.title(pageTitle))
const ogImage = topic().pic const ogImage = topic().pic
? getImageUrl(topic().pic, { width: 1200 }) ? getImageUrl(topic().pic, { width: 1200 })
@ -101,7 +97,7 @@ export const TopicView = (props: Props) => {
return ( return (
<div class={styles.topicPage}> <div class={styles.topicPage}>
<Meta name="descprition" content={description} /> <Meta name="descprition" content={description} />
<Meta name="keywords" content={t('topicKeywords', { topic: topic().title })} /> <Meta name="keywords" content={t('topicKeywords', { topic: pageTitle })} />
<Meta name="og:type" content="article" /> <Meta name="og:type" content="article" />
<Meta name="og:title" content={ogTitle} /> <Meta name="og:title" content={ogTitle} />
<Meta name="og:image" content={ogImage} /> <Meta name="og:image" content={ogImage} />
@ -110,102 +106,98 @@ export const TopicView = (props: Props) => {
<Meta name="twitter:card" content="summary_large_image" /> <Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content={ogTitle} /> <Meta name="twitter:title" content={ogTitle} />
<Meta name="twitter:description" content={description} /> <Meta name="twitter:description" content={description} />
<Show when={props.isLoaded} fallback={<Loading />}> <FullTopic topic={topic()} />
<Show when={topic()}> <div class="wide-container">
<FullTopic topic={topic()} /> <div class={clsx(styles.groupControls, 'row group__controls')}>
<div class="wide-container"> <div class="col-md-16">
<div class={clsx(styles.groupControls, 'row group__controls')}> <ul class="view-switcher">
<div class="col-md-16"> <li
<ul class="view-switcher"> classList={{
<li 'view-switcher__item--selected': searchParams().by === 'recent' || !searchParams().by,
classList={{ }}
'view-switcher__item--selected': searchParams().by === 'recent' || !searchParams().by, >
}} <button
> type="button"
<button onClick={() =>
type="button" changeSearchParams({
onClick={() => by: 'recent',
changeSearchParams({ })
by: 'recent', }
}) >
} {t('Recent')}
> </button>
{t('Recent')} </li>
</button> {/*TODO: server sort*/}
</li> {/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'rating' }}>*/}
{/*TODO: server sort*/} {/* <button type="button" onClick={() => changeSearchParams('by', 'rating')}>*/}
{/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'rating' }}>*/} {/* {t('Popular')}*/}
{/* <button type="button" onClick={() => changeSearchParams('by', 'rating')}>*/} {/* </button>*/}
{/* {t('Popular')}*/} {/*</li>*/}
{/* </button>*/} {/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'viewed' }}>*/}
{/*</li>*/} {/* <button type="button" onClick={() => changeSearchParams('by', 'viewed')}>*/}
{/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'viewed' }}>*/} {/* {t('Views')}*/}
{/* <button type="button" onClick={() => changeSearchParams('by', 'viewed')}>*/} {/* </button>*/}
{/* {t('Views')}*/} {/*</li>*/}
{/* </button>*/} {/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'commented' }}>*/}
{/*</li>*/} {/* <button type="button" onClick={() => changeSearchParams('by', 'commented')}>*/}
{/*<li classList={{ 'view-switcher__item--selected': getSearchParams().by === 'commented' }}>*/} {/* {t('Discussing')}*/}
{/* <button type="button" onClick={() => changeSearchParams('by', 'commented')}>*/} {/* </button>*/}
{/* {t('Discussing')}*/} {/*</li>*/}
{/* </button>*/} </ul>
{/*</li>*/} </div>
</ul> <div class="col-md-8">
</div> <div class="mode-switcher">
<div class="col-md-8"> {`${t('Show')} `}
<div class="mode-switcher"> <span class="mode-switcher__control">{t('All posts')}</span>
{`${t('Show')} `}
<span class="mode-switcher__control">{t('All posts')}</span>
</div>
</div>
</div> </div>
</div> </div>
</div>
</div>
<Row1 article={sortedArticles()[0]} /> <Row1 article={sortedArticles()[0]} />
<Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} /> <Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} />
<Beside <Beside
title={t('Topic is supported by')} title={t('Topic is supported by')}
values={authorsByTopic()[topic().slug].slice(0, 6)} values={authorsByTopic()[topic().slug].slice(0, 6)}
beside={sortedArticles()[4]} beside={sortedArticles()[4]}
wrapper={'author'} wrapper={'author'}
/> />
<ArticleCardSwiper title={title()} slides={sortedArticles().slice(5, 11)} /> <ArticleCardSwiper title={title()} slides={sortedArticles().slice(5, 11)} />
<Beside <Beside
beside={sortedArticles()[12]} beside={sortedArticles()[12]}
title={t('Top viewed')} title={t('Top viewed')}
values={sortedArticles().slice(0, 5)} values={sortedArticles().slice(0, 5)}
wrapper={'top-article'} wrapper={'top-article'}
/> />
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} /> <Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
<Row1 article={sortedArticles()[15]} /> <Row1 article={sortedArticles()[15]} />
<Show when={sortedArticles().length > 15}> <Show when={sortedArticles().length > 15}>
<ArticleCardSwiper slides={sortedArticles().slice(16, 22)} /> <ArticleCardSwiper slides={sortedArticles().slice(16, 22)} />
<Row3 articles={sortedArticles().slice(23, 26)} /> <Row3 articles={sortedArticles().slice(23, 26)} />
<Row2 articles={sortedArticles().slice(26, 28)} /> <Row2 articles={sortedArticles().slice(26, 28)} />
</Show> </Show>
<For each={pages()}> <For each={pages()}>
{(page) => ( {(page) => (
<> <>
<Row3 articles={page.slice(0, 3)} /> <Row3 articles={page.slice(0, 3)} />
<Row3 articles={page.slice(3, 6)} /> <Row3 articles={page.slice(3, 6)} />
<Row3 articles={page.slice(6, 9)} /> <Row3 articles={page.slice(6, 9)} />
</> </>
)} )}
</For> </For>
<Show when={isLoadMoreButtonVisible()}> <Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container"> <p class="load-more-container">
<button class="button" onClick={loadMore}> <button class="button" onClick={loadMore}>
{t('Load more')} {t('Load more')}
</button> </button>
</p> </p>
</Show>
</Show>
</Show> </Show>
</div> </div>
) )

View File

@ -5,6 +5,7 @@ import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
import { Shout } from '../../../graphql/types.gen' import { Shout } from '../../../graphql/types.gen'
import { ArticleCard } from '../../Feed/ArticleCard' import { ArticleCard } from '../../Feed/ArticleCard'
import { Icon } from '../Icon' import { Icon } from '../Icon'
import { ShowOnlyOnClient } from '../ShowOnlyOnClient'
import { SwiperRef } from './swiper' import { SwiperRef } from './swiper'
@ -25,65 +26,67 @@ export const ArticleCardSwiper = (props: Props) => {
}) })
return ( return (
<div class={clsx(styles.Swiper, styles.articleMode, styles.ArticleCardSwiper)}> <ShowOnlyOnClient>
<Show when={props.title}> <div class={clsx(styles.Swiper, styles.articleMode, styles.ArticleCardSwiper)}>
<h2 class={styles.sliderTitle}>{props.title}</h2> <Show when={props.title}>
</Show> <h2 class={styles.sliderTitle}>{props.title}</h2>
<div class={styles.container}>
<Show when={props.slides.length > 0}>
<div class={styles.holder}>
<swiper-container
ref={(el) => (mainSwipeRef.current = el)}
centered-slides={true}
observer={true}
space-between={10}
breakpoints={{
576: { spaceBetween: 20, slidesPerView: 1.5 },
992: { spaceBetween: 52, slidesPerView: 1.5 },
}}
round-lengths={true}
loop={true}
speed={800}
autoplay={{
disableOnInteraction: false,
delay: 6000,
pauseOnMouseEnter: true,
}}
>
<For each={props.slides}>
{(slide, index) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<swiper-slide virtual-index={index()}>
<ArticleCard
article={slide}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true,
}}
desktopCoverSize="L"
/>
</swiper-slide>
)}
</For>
</swiper-container>
<div
class={clsx(styles.navigation, styles.prev)}
onClick={() => mainSwipeRef.current.swiper.slidePrev()}
>
<Icon name="swiper-l-arr" class={styles.icon} />
</div>
<div
class={clsx(styles.navigation, styles.next)}
onClick={() => mainSwipeRef.current.swiper.slideNext()}
>
<Icon name="swiper-r-arr" class={styles.icon} />
</div>
</div>
</Show> </Show>
<div class={styles.container}>
<Show when={props.slides.length > 0}>
<div class={styles.holder}>
<swiper-container
ref={(el) => (mainSwipeRef.current = el)}
centered-slides={true}
observer={true}
space-between={10}
breakpoints={{
576: { spaceBetween: 20, slidesPerView: 1.5 },
992: { spaceBetween: 52, slidesPerView: 1.5 },
}}
round-lengths={true}
loop={true}
speed={800}
autoplay={{
disableOnInteraction: false,
delay: 6000,
pauseOnMouseEnter: true,
}}
>
<For each={props.slides}>
{(slide, index) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<swiper-slide virtual-index={index()}>
<ArticleCard
article={slide}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true,
}}
desktopCoverSize="L"
/>
</swiper-slide>
)}
</For>
</swiper-container>
<div
class={clsx(styles.navigation, styles.prev)}
onClick={() => mainSwipeRef.current.swiper.slidePrev()}
>
<Icon name="swiper-l-arr" class={styles.icon} />
</div>
<div
class={clsx(styles.navigation, styles.next)}
onClick={() => mainSwipeRef.current.swiper.slideNext()}
>
<Icon name="swiper-r-arr" class={styles.icon} />
</div>
</div>
</Show>
</div>
</div> </div>
</div> </ShowOnlyOnClient>
) )
} }

View File

@ -3,6 +3,7 @@ import type { PageContext } from '../renderer/types'
import { render } from 'vike/abort' import { render } from 'vike/abort'
import { PRERENDERED_ARTICLES_COUNT } from '../components/Views/Topic'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
export const onBeforeRender = async (pageContext: PageContext) => { export const onBeforeRender = async (pageContext: PageContext) => {
@ -14,7 +15,12 @@ export const onBeforeRender = async (pageContext: PageContext) => {
throw render(404) throw render(404)
} }
const pageProps: PageProps = { topic, seo: { title: topic.title } } const topicShouts = await apiClient.getShouts({
filters: { topic: topic.slug },
limit: PRERENDERED_ARTICLES_COUNT,
})
const pageProps: PageProps = { topic, topicShouts, seo: { title: topic.title } }
return { return {
pageContext: { pageContext: {

View File

@ -1,7 +1,8 @@
import type { PageProps } from './types' import type { PageProps } from './types'
import { createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' import { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show } from 'solid-js'
import { Loading } from '../components/_shared/Loading'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../components/Views/Topic' import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../components/Views/Topic'
import { ReactionsProvider } from '../context/reactions' import { ReactionsProvider } from '../context/reactions'
@ -16,7 +17,7 @@ export const TopicPage = (props: PageProps) => {
const [isLoaded, setIsLoaded] = createSignal( const [isLoaded, setIsLoaded] = createSignal(
Boolean(props.topicShouts) && Boolean(props.topic) && props.topic.slug === slug(), Boolean(props.topicShouts) && Boolean(props.topic) && props.topic.slug === slug(),
) )
const [pageTitle, setPageTitle] = createSignal<string>()
const preload = () => const preload = () =>
Promise.all([ Promise.all([
loadShouts({ filters: { topic: slug() }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 }), loadShouts({ filters: { topic: slug() }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 }),
@ -51,15 +52,15 @@ export const TopicPage = (props: PageProps) => {
const usePrerenderedData = props.topic?.slug === slug() const usePrerenderedData = props.topic?.slug === slug()
return ( return (
<PageLayout title={pageTitle()}> <PageLayout title={props.seo.title}>
<ReactionsProvider> <ReactionsProvider>
<TopicView <Show when={isLoaded()} fallback={<Loading />}>
title={(title) => setPageTitle(title)} <TopicView
isLoaded={isLoaded()} topic={usePrerenderedData ? props.topic : null}
topic={usePrerenderedData ? props.topic : null} shouts={usePrerenderedData ? props.topicShouts : null}
shouts={usePrerenderedData ? props.topicShouts : null} topicSlug={slug()}
topicSlug={slug()} />
/> </Show>
</ReactionsProvider> </ReactionsProvider>
</PageLayout> </PageLayout>
) )