expo-fixes

This commit is contained in:
Untone 2024-09-26 01:48:30 +03:00
parent 22575cc7fa
commit a144d7051b
7 changed files with 155 additions and 201 deletions

View File

@ -3,12 +3,4 @@
background: #fef2f2; background: #fef2f2;
padding: 0 0 4rem; padding: 0 0 4rem;
min-height: 100vh; min-height: 100vh;
.showMore {
display: flex;
width: 100%;
padding: 4rem 0 2rem;
align-items: center;
justify-content: center;
}
} }

View File

@ -1,47 +1,34 @@
import { A } from '@solidjs/router' import { For, Show, createEffect, createSignal, on } from 'solid-js'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper' import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper'
import { EXPO_LAYOUTS, SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { EXPO_LAYOUTS, SHOUTS_PER_PAGE } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { loadShouts } from '~/graphql/api/public'
import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top'
import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
import { LayoutType } from '~/types/common' import { ExpoLayoutType } from '~/types/common'
import { getUnixtime } from '~/utils/date' import { getUnixtime } from '~/utils/date'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { byCreated } from '~/utils/sort'
import { ArticleCard } from '../../Feed/ArticleCard' import { ArticleCard } from '../../Feed/ArticleCard'
import styles from './Expo.module.scss' import styles from './Expo.module.scss'
type Props = { type Props = {
shouts: Shout[] shouts: Shout[]
topMonthShouts?: Shout[] layout: ExpoLayoutType
topRatedShouts?: Shout[]
layout?: LayoutType
} }
export const PRERENDERED_ARTICLES_COUNT = 36
const LOAD_MORE_PAGE_SIZE = 12
export const Expo = (props: Props) => { export const Expo = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { client } = useSession() const { client } = useSession()
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([]) const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
const { feedByLayout, expoFeed, setExpoFeed } = useFeed()
const layouts = createMemo<LayoutType[]>(() => (props.layout ? [props.layout] : EXPO_LAYOUTS))
// Функция загрузки случайных избранных статей
const loadRandomTopArticles = async () => { const loadRandomTopArticles = async () => {
const layouts = props.layout ? [props.layout] : EXPO_LAYOUTS
const options: LoadShoutsOptions = { const options: LoadShoutsOptions = {
filters: { layouts: layouts(), featured: true }, filters: { layouts, featured: true },
limit: 10, limit: 10,
random_limit: 100 random_limit: 100
} }
@ -49,11 +36,13 @@ export const Expo = (props: Props) => {
setFavoriteTopArticles(resp?.data?.load_shouts_random_top || []) setFavoriteTopArticles(resp?.data?.load_shouts_random_top || [])
} }
// Функция загрузки популярных статей за последний месяц
const loadRandomTopMonthArticles = async () => { const loadRandomTopMonthArticles = async () => {
const layouts = props.layout ? [props.layout] : EXPO_LAYOUTS
const now = new Date() const now = new Date()
const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1))) const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1)))
const options: LoadShoutsOptions = { const options: LoadShoutsOptions = {
filters: { layouts: layouts(), after, reacted: true }, filters: { layouts, after, reacted: true },
limit: 10, limit: 10,
random_limit: 10 random_limit: 10
} }
@ -61,141 +50,46 @@ export const Expo = (props: Props) => {
setReactedTopMonthArticles(resp?.data?.load_shouts_random_top || []) setReactedTopMonthArticles(resp?.data?.load_shouts_random_top || [])
} }
onMount(() => { // Эффект для загрузки random top при изменении layout
loadRandomTopArticles()
loadRandomTopMonthArticles()
})
createEffect( createEffect(
on(layouts, (lll) => { on(
console.debug('layouts changed', lll) () => props.layout,
loadRandomTopArticles() async (_layout?: ExpoLayoutType) => {
loadRandomTopMonthArticles() await loadRandomTopArticles()
}) await loadRandomTopMonthArticles()
)
onCleanup(() => {
setExpoFeed([])
})
const ExpoTabs = () => (
<div class="wide-container">
<ul class={clsx('view-switcher')}>
<li class={clsx({ 'view-switcher__item--selected': !props.layout })}>
<A href={'/expo'}>
<span class={clsx('linkReplacement')}>{t('All')}</span>
</A>
</li>
<li class={clsx({ 'view-switcher__item--selected': props.layout === 'literature' })}>
<ConditionalWrapper
condition={props.layout !== 'literature'}
wrapper={(children) => <A href={'/expo/literature'}>{children}</A>}
>
<span class={clsx('linkReplacement')}>{t('Literature')}</span>
</ConditionalWrapper>
</li>
<li class={clsx({ 'view-switcher__item--selected': props.layout === 'audio' })}>
<ConditionalWrapper
condition={props.layout !== 'audio'}
wrapper={(children) => <A href={'/expo/audio'}>{children}</A>}
>
<span class={clsx('linkReplacement')}>{t('Music')}</span>
</ConditionalWrapper>
</li>
<li class={clsx({ 'view-switcher__item--selected': props.layout === 'image' })}>
<ConditionalWrapper
condition={props.layout !== 'image'}
wrapper={(children) => <A href={'/expo/image'}>{children}</A>}
>
<span class={clsx('linkReplacement')}>{t('Gallery')}</span>
</ConditionalWrapper>
</li>
<li class={clsx({ 'view-switcher__item--selected': props.layout === 'video' })}>
<ConditionalWrapper
condition={props.layout !== 'video'}
wrapper={(children) => <A href={'/expo/video'}>{children}</A>}
>
<span class={clsx('cursorPointer linkReplacement')}>{t('Video')}</span>
</ConditionalWrapper>
</li>
</ul>
</div>
)
const ExpoGrid = (props: Props) => (
<div class="wide-container">
<div class="row">
<For each={expoFeed()?.slice(0, LOAD_MORE_PAGE_SIZE) || []}>
{(shout) => (
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard
article={shout}
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
desktopCoverSize="XS"
withAspectRatio={true}
/>
</div>
)}
</For>
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
<ArticleCardSwiper title={t('Top month')} slides={reactedTopMonthArticles()} />
</Show>
<For each={(props.topMonthShouts || []).slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
{(shout) => (
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard
article={shout}
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
desktopCoverSize="XS"
withAspectRatio={true}
/>
</div>
)}
</For>
<Show when={favoriteTopArticles()?.length > 0} keyed={true}>
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
</Show>
<For each={props.topRatedShouts?.slice(LOAD_MORE_PAGE_SIZE * 2, expoFeed()?.length || 0)}>
{(shout) => (
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard
article={shout}
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
desktopCoverSize="XS"
withAspectRatio={true}
/>
</div>
)}
</For>
</div>
</div>
)
const [loadMoreVisible, setLoadMoreVisible] = createSignal(false)
// дозагрузка
const loadMore = async () => {
saveScrollPosition()
const limit = SHOUTS_PER_PAGE
const offset = (props.layout ? feedByLayout()[props.layout] : expoFeed())?.length
const filters: LoadShoutsFilters = { layouts: layouts(), featured: true }
const options: LoadShoutsOptions = { filters, limit, offset }
const shoutsFetcher = loadShouts(options)
const result = await shoutsFetcher()
setLoadMoreVisible(Boolean(result?.length))
const expoFeedUpdater = (layout?: LayoutType) => (prev: Shout[]) =>
Array.from(new Set((layout ? prev || [] : expoFeed())?.concat(result || [])))?.sort(byCreated)
result && setExpoFeed(expoFeedUpdater(props.layout))
restoreScrollPosition()
return result as LoadMoreItems
} }
)
)
return ( return (
<div class={styles.Expo}> <div class={styles.Expo}>
<ExpoTabs /> <Show when={props.shouts} fallback={<Loading />} keyed>
{(feed: Shout[]) => (
<div class="wide-container">
<div class="row">
<For each={feed.slice(0, SHOUTS_PER_PAGE) || []}>
{(shout) => (
<div id={`shout-${shout.id}`} class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard
article={shout}
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
desktopCoverSize="XS"
withAspectRatio={true}
/>
</div>
)}
</For>
</div>
<Show when={expoFeed()} fallback={<Loading />}> <Show when={reactedTopMonthArticles()?.length > 0}>
<LoadMoreWrapper loadFunction={loadMore} pageSize={LOAD_MORE_PAGE_SIZE} hidden={!loadMoreVisible()}> <ArticleCardSwiper title={t('Top month')} slides={reactedTopMonthArticles()} />
<ExpoGrid {...props} /> </Show>
</LoadMoreWrapper>
<Show when={favoriteTopArticles()?.length > 0}>
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
</Show>
</div>
)}
</Show> </Show>
</div> </div>
) )

View File

@ -0,0 +1,34 @@
import { A } from '@solidjs/router'
import { clsx } from 'clsx'
import { For } from 'solid-js'
import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
import { EXPO_LAYOUTS, EXPO_TITLES } from '~/context/feed'
import { useLocalize } from '~/context/localize'
import { ExpoLayoutType } from '~/types/common'
export const ExpoNav = (props: { layout: ExpoLayoutType | '' }) => {
const { t } = useLocalize()
return (
<div class="wide-container">
<ul class={clsx('view-switcher')}>
<For each={[...EXPO_LAYOUTS, '']}>
{(layoutKey) => (
<li class={clsx({ 'view-switcher__item--selected': props.layout === layoutKey })}>
<ConditionalWrapper
condition={props.layout !== layoutKey}
wrapper={(children) => <A href={`/expo/${layoutKey}`}>{children}</A>}
>
<span class="linkReplacement">
{layoutKey in EXPO_TITLES ? t(EXPO_TITLES[layoutKey as ExpoLayoutType]) : t('All')}
</span>
</ConditionalWrapper>
</li>
)}
</For>
</ul>
</div>
)
}
export default ExpoNav

View File

@ -33,6 +33,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
) )
const loadItems = async () => { const loadItems = async () => {
// console.debug('LoadMoreWrapper.loadItems offset:', offset())
setIsLoading(true) setIsLoading(true)
saveScrollPosition() saveScrollPosition()
const newItems = await props.loadFunction(offset()) const newItems = await props.loadFunction(offset())
@ -47,6 +48,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
) )
setIsLoading(false) setIsLoading(false)
restoreScrollPosition() restoreScrollPosition()
// console.debug('LoadMoreWrapper.loadItems loaded:', newItems.length)
} }
onMount(loadItems) onMount(loadItems)
@ -54,6 +56,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
return ( return (
<> <>
{props.children} {props.children}
<div>
<Show when={isLoadMoreButtonVisible() && !props.hidden}> <Show when={isLoadMoreButtonVisible() && !props.hidden}>
<div class="load-more-container"> <div class="load-more-container">
<Button <Button
@ -64,6 +67,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
/> />
</div> </div>
</Show> </Show>
</div>
</> </>
) )
} }

View File

@ -10,13 +10,20 @@ import {
Shout, Shout,
Topic Topic
} from '~/graphql/schema/core.gen' } from '~/graphql/schema/core.gen'
import { LayoutType } from '~/types/common' import { ExpoLayoutType } from '~/types/common'
import { byStat } from '../utils/sort' import { byStat } from '../utils/sort'
import { useSession } from './session' import { useSession } from './session'
export const PRERENDERED_ARTICLES_COUNT = 5 export const PRERENDERED_ARTICLES_COUNT = 5
export const SHOUTS_PER_PAGE = 20 export const SHOUTS_PER_PAGE = 20
export const EXPO_LAYOUTS = ['audio', 'literature', 'video', 'image'] as LayoutType[] export const EXPO_LAYOUTS = ['audio', 'literature', 'video', 'image'] as ExpoLayoutType[]
export const EXPO_TITLES: Record<ExpoLayoutType | '', string> = {
'audio': 'Audio',
'video': 'Video',
'image': 'Artworks',
'literature': 'Literature',
'': 'All'
}
type FeedContextType = { type FeedContextType = {
sortedFeed: Accessor<Shout[]> sortedFeed: Accessor<Shout[]>

View File

@ -1,13 +1,17 @@
import { Params, RouteSectionProps, createAsync } from '@solidjs/router' import { Params, RouteSectionProps, createAsync } from '@solidjs/router'
import { Show, onMount } from 'solid-js' import { Show, createEffect, createSignal, on } from 'solid-js'
import { TopicsNav } from '~/components/TopicsNav' import { TopicsNav } from '~/components/TopicsNav'
import { Expo } from '~/components/Views/Expo' import { Expo } from '~/components/Views/Expo'
import ExpoNav from '~/components/Views/Expo/ExpoNav'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { EXPO_LAYOUTS, SHOUTS_PER_PAGE } from '~/context/feed' import { EXPO_LAYOUTS, EXPO_TITLES, SHOUTS_PER_PAGE, useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { loadShouts } from '~/graphql/api/public' import { loadShouts } from '~/graphql/api/public'
import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
import { LayoutType } from '~/types/common' import { ExpoLayoutType } from '~/types/common'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { byCreated } from '~/utils/sort'
const fetchExpoShouts = async (layouts: string[]) => { const fetchExpoShouts = async (layouts: string[]) => {
const result = await loadShouts({ const result = await loadShouts({
@ -28,35 +32,50 @@ export const route = {
export default (props: RouteSectionProps<Shout[]>) => { export default (props: RouteSectionProps<Shout[]>) => {
const { t } = useLocalize() const { t } = useLocalize()
const { expoFeed, setExpoFeed, feedByLayout } = useFeed()
const [loadMoreVisible, setLoadMoreVisible] = createSignal(false)
const getTitle = (l?: string) => EXPO_TITLES[(l as ExpoLayoutType) || '']
const shouts = createAsync( const shouts = createAsync(
async () => async () =>
props.data || (await fetchExpoShouts(props.params.layout ? [props.params.layout] : EXPO_LAYOUTS)) props.data || (await fetchExpoShouts(props.params.layout ? [props.params.layout] : EXPO_LAYOUTS))
) )
const getTitle = (l: string) => { // Функция для загрузки дополнительных шотов
switch (l) { const loadMore = async () => {
case 'audio': { saveScrollPosition()
return t('Audio') const limit = SHOUTS_PER_PAGE
const layouts = props.params.layout ? [props.params.layout] : EXPO_LAYOUTS
const offset = expoFeed()?.length || 0
const filters: LoadShoutsFilters = { layouts, featured: true }
const options: LoadShoutsOptions = { filters, limit, offset }
const shoutsFetcher = loadShouts(options)
const result = await shoutsFetcher()
setLoadMoreVisible(Boolean(result?.length))
if (result) {
setExpoFeed((prev) => Array.from(new Set([...(prev || []), ...result])).sort(byCreated))
} }
case 'video': { restoreScrollPosition()
return t('Video') return result as LoadMoreItems
} }
case 'image': { // Эффект для загрузки данных при изменении layout
return t('Artworks') createEffect(
on(
() => props.params.layout as ExpoLayoutType,
async (layout?: ExpoLayoutType) => {
const layouts = layout ? [layout] : EXPO_LAYOUTS
const offset = (layout ? feedByLayout()[layout]?.length : expoFeed()?.length) || 0
const options: LoadShoutsOptions = {
filters: { layouts, featured: true },
limit: SHOUTS_PER_PAGE,
offset
} }
case 'literature': { const shoutsFetcher = loadShouts(options)
return t('Literature') const result = await shoutsFetcher()
setExpoFeed(result || [])
} }
default: { )
return t('Art') )
}
}
}
onMount(() => {
document.title = getTitle(props.params.layout || '')
})
return ( return (
<PageLayout <PageLayout
withPadding={true} withPadding={true}
@ -64,9 +83,12 @@ export default (props: RouteSectionProps<Shout[]>) => {
title={`${t('Discours')} :: ${getTitle(props.params.layout || '')}`} title={`${t('Discours')} :: ${getTitle(props.params.layout || '')}`}
> >
<TopicsNav /> <TopicsNav />
<ExpoNav layout={(props.params.layout || '') as ExpoLayoutType | ''} />
<LoadMoreWrapper loadFunction={loadMore} pageSize={SHOUTS_PER_PAGE} hidden={!loadMoreVisible()}>
<Show when={shouts()} keyed> <Show when={shouts()} keyed>
{(sss) => <Expo shouts={sss} layout={props.params.layout as LayoutType} />} {(sss: Shout[]) => <Expo shouts={sss} layout={props.params.layout as ExpoLayoutType} />}
</Show> </Show>
</LoadMoreWrapper>
</PageLayout> </PageLayout>
) )
} }

View File

@ -4,7 +4,8 @@ export type RootSearchParams = {
token: string; token: string;
}; };
export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature'; export type ExpoLayoutType = 'audio' | 'video' | 'image' | 'literature';
export type LayoutType = 'article' | ExpoLayoutType;
export type FollowsFilter = 'all' | 'authors' | 'topics' | 'communities'; export type FollowsFilter = 'all' | 'authors' | 'topics' | 'communities';
export type SortFunction<T> = (a: T, b: T) => number export type SortFunction<T> = (a: T, b: T) => number
export type FilterFunction<T> = (a: T) => boolean export type FilterFunction<T> = (a: T) => boolean