Feature/expo pages (#253)

Expo pages
This commit is contained in:
Ilya Y 2023-10-10 18:38:02 +03:00 committed by GitHub
parent 796321e3ee
commit 44c2269edb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 383 additions and 313 deletions

View File

@ -29,6 +29,7 @@
"All topics": "All topics", "All topics": "All topics",
"Almost done! Check your email.": "Almost done! Just checking your email.", "Almost done! Check your email.": "Almost done! Just checking your email.",
"Are you sure you want to to proceed the action?": "Are you sure you want to to proceed the action?", "Are you sure you want to to proceed the action?": "Are you sure you want to to proceed the action?",
"Art": "Art",
"Artist": "Artist", "Artist": "Artist",
"Artworks": "Artworks", "Artworks": "Artworks",
"Audio": "Audio", "Audio": "Audio",
@ -134,6 +135,7 @@
"Forgot password?": "Forgot your password?", "Forgot password?": "Forgot your password?",
"Forward": "Forward", "Forward": "Forward",
"Full name": "First and last name", "Full name": "First and last name",
"Gallery": "Gallery",
"Gallery name": "Gallery name", "Gallery name": "Gallery name",
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine", "Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine",
"Go to main page": "Go to main page", "Go to main page": "Go to main page",
@ -199,6 +201,7 @@
"Most read": "Readable", "Most read": "Readable",
"Move down": "Move down", "Move down": "Move down",
"Move up": "Move up", "Move up": "Move up",
"Music": "Music",
"My feed": "My feed", "My feed": "My feed",
"My subscriptions": "Subscriptions", "My subscriptions": "Subscriptions",
"Name": "Name", "Name": "Name",

View File

@ -32,6 +32,7 @@
"All topics": "Все темы", "All topics": "Все темы",
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.", "Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
"Are you sure you want to to proceed the action?": "Вы уверены, что хотите продолжить?", "Are you sure you want to to proceed the action?": "Вы уверены, что хотите продолжить?",
"Art": "Искусство",
"Artist": "Исполнитель", "Artist": "Исполнитель",
"Artist...": "Исполнитель...", "Artist...": "Исполнитель...",
"Artworks": "Артворки", "Artworks": "Артворки",
@ -139,6 +140,7 @@
"Forgot password?": "Забыли пароль?", "Forgot password?": "Забыли пароль?",
"Forward": "Переслать", "Forward": "Переслать",
"Full name": "Имя и фамилия", "Full name": "Имя и фамилия",
"Gallery": "Галерея",
"Gallery name": "Название галереи", "Gallery name": "Название галереи",
"Genre...": "Жанр...", "Genre...": "Жанр...",
"Get notifications": "Получать уведомления", "Get notifications": "Получать уведомления",
@ -209,6 +211,7 @@
"Most read": "Читаемое", "Most read": "Читаемое",
"Move down": "Переместить вниз", "Move down": "Переместить вниз",
"Move up": "Переместить вверх", "Move up": "Переместить вверх",
"Music": "Музыка",
"My feed": "Моя лента", "My feed": "Моя лента",
"My subscriptions": "Подписки", "My subscriptions": "Подписки",
"Name": "Имя", "Name": "Имя",

View File

@ -30,7 +30,7 @@ import { CreatePage } from '../pages/create.page'
import { EditPage } from '../pages/edit.page' import { EditPage } from '../pages/edit.page'
import { ConnectPage } from '../pages/connect.page' import { ConnectPage } from '../pages/connect.page'
import { InboxPage } from '../pages/inbox.page' import { InboxPage } from '../pages/inbox.page'
import { LayoutShoutsPage } from '../pages/layoutShouts.page' import { ExpoPage } from '../pages/expo/expo.page'
import { SessionProvider } from '../context/session' import { SessionProvider } from '../context/session'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page' import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page' import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
@ -51,7 +51,8 @@ const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
authorFollowing: AuthorPage, authorFollowing: AuthorPage,
authorFollowers: AuthorPage, authorFollowers: AuthorPage,
inbox: InboxPage, inbox: InboxPage,
expo: LayoutShoutsPage, expo: ExpoPage,
expoLayout: ExpoPage,
connect: ConnectPage, connect: ConnectPage,
create: CreatePage, create: CreatePage,
edit: EditPage, edit: EditPage,

View File

@ -218,7 +218,7 @@ export const FullArticle = (props: Props) => {
<div class="row position-relative"> <div class="row position-relative">
<article class="col-md-16 col-lg-14 col-xl-12 offset-md-5"> <article class="col-md-16 col-lg-14 col-xl-12 offset-md-5">
{/*TODO: Check styles.shoutTopic*/} {/*TODO: Check styles.shoutTopic*/}
<Show when={props.article.layout !== 'audio'}> <Show when={props.article.layout !== 'music'}>
<div class={styles.shoutHeader}> <div class={styles.shoutHeader}>
<Show when={mainTopic()}> <Show when={mainTopic()}>
<CardTopic title={mainTopic().title} slug={props.article.mainTopic} /> <CardTopic title={mainTopic().title} slug={props.article.mainTopic} />
@ -256,7 +256,7 @@ export const FullArticle = (props: Props) => {
<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 === 'music'}>
<AudioHeader <AudioHeader
title={props.article.title} title={props.article.title}
cover={props.article.cover} cover={props.article.cover}

View File

@ -375,7 +375,7 @@ export const Header = (props: Props) => {
> >
<ul class="nodash"> <ul class="nodash">
<li class="item"> <li class="item">
<a href="/expo/image">Искусство</a> <a href="/expo">{t('Art')}</a>
</li> </li>
<li class="item"> <li class="item">
<a href="/podcasts">Подкасты</a> <a href="/podcasts">Подкасты</a>

View File

@ -1,52 +0,0 @@
import { Icon } from '../_shared/Icon'
import { useLocalize } from '../../context/localize'
import './Topics.scss'
export const NavTopics = () => {
const { t } = useLocalize()
return (
<nav class="subnavigation wide-container text-2xl">
<ul class="topics">
<li class="item">
<a href="/expo/image">Искусство</a>
</li>
<li class="item">
<a href="/podcasts">Подкасты</a>
</li>
<li class="item">
<a href="">Спецпроекты</a>
</li>
<li class="item">
<a href="/topic/interview">#Интервью</a>
</li>
<li class="item">
<a href="/topic/reportage">#Репортажи</a>
</li>
<li class="item">
<a href="/topic/empiric">#Личный опыт</a>
</li>
<li class="item">
<a href="/topic/society">#Общество</a>
</li>
<li class="item">
<a href="/topic/culture">#Культура</a>
</li>
<li class="item">
<a href="/topic/theory">#Теории</a>
</li>
<li class="item">
<a href="/topic/poetry">#Поэзия</a>
</li>
<li class="item right">
<a href={`/topics`}>
<span>
{t('All topics')}
<Icon name="arrow-right-black" class={'icon'} />
</span>
</a>
</li>
</ul>
</nav>
)
}

View File

@ -1,8 +1,9 @@
.subnavigation { .Topics {
@include font-size(1.4rem); @include font-size(1.4rem);
height: 6rem; height: 6rem;
line-height: 6rem; line-height: 6rem;
margin-bottom: 5rem !important; margin-bottom: 3rem;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
transform: translateY(-2px); transform: translateY(-2px);
@ -11,7 +12,7 @@
padding: 0 divide($container-padding-x, 2); padding: 0 divide($container-padding-x, 2);
} }
.topics { .list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
font-weight: 500; font-weight: 500;
@ -32,32 +33,11 @@
} }
a { a {
border-bottom: 0; border-bottom: unset;
&.selected {
&:hover { font-weight: 500;
.icon { border-bottom: 2px solid var(--default-color);
filter: invert(1);
} }
} }
} }
.icon {
display: inline-block;
margin-left: 0.3em;
position: relative;
top: 0.15em;
}
}
.selected {
color: black;
}
a {
color: #141414;
}
a:hover {
color: white;
}
} }

View File

@ -0,0 +1,57 @@
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import styles from './Topics.module.scss'
import { clsx } from 'clsx'
import { router, useRouter } from '../../../stores/router'
import { getPagePath } from '@nanostores/router'
export const Topics = () => {
const { t } = useLocalize()
const { page } = useRouter()
return (
<nav class={clsx('wide-container text-2xl', styles.Topics)}>
<ul class={styles.list}>
<li class={styles.item}>
<a class={clsx({ [styles.selected]: page().route === 'expo' })} href="/expo">
{t('Art')}
</a>
</li>
<li class={styles.item}>
<a href="/podcasts">Подкасты</a>
</li>
<li class={styles.item}>
<a href="">Спецпроекты</a>
</li>
<li class={styles.item}>
<a href="/topic/interview">#Интервью</a>
</li>
<li class={styles.item}>
<a href="/topic/reportage">#Репортажи</a>
</li>
<li class={styles.item}>
<a href="/topic/empiric">#Личный опыт</a>
</li>
<li class={styles.item}>
<a href="/topic/society">#Общество</a>
</li>
<li class={styles.item}>
<a href="/topic/culture">#Культура</a>
</li>
<li class={styles.item}>
<a href="/topic/theory">#Теории</a>
</li>
<li class={styles.item}>
<a href="/topic/poetry">#Поэзия</a>
</li>
<li class={clsx(styles.item, styles.right)}>
<a href={getPagePath(router, 'topics')}>
<span>
{t('All topics')}
<Icon name="arrow-right-black" class={'icon'} />
</span>
</a>
</li>
</ul>
</nav>
)
}

View File

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

View File

@ -3,7 +3,6 @@ import styles from './TopicBadge.module.scss'
import { FollowingEntity, Topic } from '../../../graphql/types.gen' import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { createMemo, createSignal, Show } from 'solid-js' import { createMemo, createSignal, Show } from 'solid-js'
import { imageProxy } from '../../../utils/imageProxy' import { imageProxy } from '../../../utils/imageProxy'
import { capitalize } from '../../../utils'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'

View File

@ -158,7 +158,7 @@ export const EditView = (props: Props) => {
const articleTitle = () => { const articleTitle = () => {
switch (props.shout.layout as LayoutType) { switch (props.shout.layout as LayoutType) {
case 'audio': { case 'music': {
return t('Album name') return t('Album name')
} }
case 'image': { case 'image': {
@ -172,7 +172,7 @@ export const EditView = (props: Props) => {
const pageTitle = () => { const pageTitle = () => {
switch (props.shout.layout as LayoutType) { switch (props.shout.layout as LayoutType) {
case 'audio': { case 'music': {
return t('Publish Album') return t('Publish Album')
} }
case 'image': { case 'image': {
@ -259,19 +259,19 @@ export const EditView = (props: Props) => {
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5"> <div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
<Show when={page().route === 'edit'}> <Show when={page().route === 'edit'}>
<div class={styles.headingActions}> <div class={styles.headingActions}>
<Show when={!isSubtitleVisible() && props.shout.layout !== 'audio'}> <Show when={!isSubtitleVisible() && props.shout.layout !== 'music'}>
<div class={styles.action} onClick={showSubtitleInput}> <div class={styles.action} onClick={showSubtitleInput}>
{t('Add subtitle')} {t('Add subtitle')}
</div> </div>
</Show> </Show>
<Show when={!isLeadVisible() && props.shout.layout !== 'audio'}> <Show when={!isLeadVisible() && props.shout.layout !== 'music'}>
<div class={styles.action} onClick={showLeadInput}> <div class={styles.action} onClick={showLeadInput}>
{t('Add intro')} {t('Add intro')}
</div> </div>
</Show> </Show>
</div> </div>
<> <>
<div class={clsx({ [styles.audioHeader]: props.shout.layout === 'audio' })}> <div class={clsx({ [styles.audioHeader]: props.shout.layout === 'music' })}>
<div class={styles.inputContainer}> <div class={styles.inputContainer}>
<GrowingTextarea <GrowingTextarea
allowEnterKey={true} allowEnterKey={true}
@ -286,7 +286,7 @@ export const EditView = (props: Props) => {
<div class={styles.validationError}>{formErrors.title}</div> <div class={styles.validationError}>{formErrors.title}</div>
</Show> </Show>
<Show when={props.shout.layout === 'audio'}> <Show when={props.shout.layout === 'music'}>
<div class={styles.additional}> <div class={styles.additional}>
<input <input
type="text" type="text"
@ -314,7 +314,7 @@ export const EditView = (props: Props) => {
/> />
</div> </div>
</Show> </Show>
<Show when={props.shout.layout !== 'audio'}> <Show when={props.shout.layout !== 'music'}>
<Show when={isSubtitleVisible()}> <Show when={isSubtitleVisible()}>
<GrowingTextarea <GrowingTextarea
textAreaRef={(el) => { textAreaRef={(el) => {
@ -340,7 +340,7 @@ export const EditView = (props: Props) => {
</Show> </Show>
</Show> </Show>
</div> </div>
<Show when={props.shout.layout === 'audio'}> <Show when={props.shout.layout === 'music'}>
<Show <Show
when={form.coverImageUrl} when={form.coverImageUrl}
fallback={ fallback={
@ -387,7 +387,7 @@ export const EditView = (props: Props) => {
/> />
</Show> </Show>
<Show when={props.shout.layout === 'audio'}> <Show when={props.shout.layout === 'music'}>
<AudioUploader <AudioUploader
audio={mediaItems()} audio={mediaItems()}
baseFields={baseAudioFields()} baseFields={baseAudioFields()}

View File

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

View File

@ -0,0 +1,174 @@
import styles from './Expo.module.scss'
import { LoadShoutsOptions, Shout } from '../../../graphql/types.gen'
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
import { ArticleCard } from '../../Feed/ArticleCard'
import { Loading } from '../../_shared/Loading'
import { Button } from '../../_shared/Button'
import { useLocalize } from '../../../context/localize'
import { router, useRouter } from '../../../stores/router'
import { LayoutType } from '../../../pages/types'
import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
import { splitToPages } from '../../../utils/splitToPages'
import { clsx } from 'clsx'
import { getPagePath } from '@nanostores/router'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
type Props = {
shouts: Shout[]
}
export const PRERENDERED_ARTICLES_COUNT = 28
const LOAD_MORE_PAGE_SIZE = 16
export const Expo = (props: Props) => {
const [isLoaded, setIsLoaded] = createSignal<boolean>(Boolean(props.shouts))
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { t } = useLocalize()
const { page: getPage } = useRouter()
const getLayout = createMemo<LayoutType>(() => getPage().params['layout'] as LayoutType)
const { sortedArticles } = useArticlesStore({
shouts: isLoaded() ? props.shouts : []
})
const loadMore = async (count) => {
saveScrollPosition()
const options: LoadShoutsOptions = {
limit: count,
offset: sortedArticles().length
}
options.filters = getLayout() ? { layout: getLayout() } : { excludeLayout: 'article' }
const { hasMore } = await loadShouts(options)
setIsLoadMoreButtonVisible(hasMore)
restoreScrollPosition()
}
const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
)
onMount(() => {
if (isLoaded()) {
return
}
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
setIsLoaded(true)
})
onMount(() => {
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
loadMore(LOAD_MORE_PAGE_SIZE)
}
})
createEffect(
on(
() => getLayout(),
() => {
resetSortedArticles()
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
},
{ defer: true }
)
)
onCleanup(() => {
resetSortedArticles()
})
const handleLoadMoreClick = () => {
loadMore(LOAD_MORE_PAGE_SIZE)
}
return (
<div class={styles.Expo}>
<Show when={sortedArticles().length > 0} fallback={<Loading />}>
<div class="wide-container">
<ul class={clsx('view-switcher', styles.navigation)}>
<li class={clsx({ 'view-switcher__item--selected': !getLayout() })}>
<ConditionalWrapper
condition={Boolean(getLayout())}
wrapper={(children) => <a href={getPagePath(router, 'expo')}>{children}</a>}
>
<span class={clsx('linkReplacement')}>{t('All')}</span>
</ConditionalWrapper>
</li>
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'literature' })}>
<ConditionalWrapper
condition={getLayout() !== 'literature'}
wrapper={(children) => (
<a href={getPagePath(router, 'expoLayout', { layout: 'literature' })}>{children}</a>
)}
>
<span class={clsx('linkReplacement')}>{t('Literature')}</span>
</ConditionalWrapper>
</li>
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'music' })}>
<ConditionalWrapper
condition={getLayout() !== 'music'}
wrapper={(children) => (
<a href={getPagePath(router, 'expoLayout', { layout: 'music' })}>{children}</a>
)}
>
<span class={clsx('linkReplacement')}>{t('Music')}</span>
</ConditionalWrapper>
</li>
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'image' })}>
<ConditionalWrapper
condition={getLayout() !== 'image'}
wrapper={(children) => (
<a href={getPagePath(router, 'expoLayout', { layout: 'image' })}>{children}</a>
)}
>
<span class={clsx('linkReplacement')}>{t('Gallery')}</span>
</ConditionalWrapper>
</li>
<li class={clsx({ 'view-switcher__item--selected': getLayout() === 'video' })}>
<ConditionalWrapper
condition={getLayout() !== 'video'}
wrapper={(children) => (
<a href={getPagePath(router, 'expoLayout', { layout: 'video' })}>{children}</a>
)}
>
<span class={clsx('cursorPointer linkReplacement')}>{t('Video')}</span>
</ConditionalWrapper>
</li>
</ul>
<div class="row">
<For each={sortedArticles().slice(0, PRERENDERED_ARTICLES_COUNT)}>
{(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 }}
/>
</div>
)}
</For>
<For each={pages()}>
{(page) => (
<For each={page}>
{(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 }}
/>
</div>
)}
</For>
)}
</For>
</div>
<Show when={isLoadMoreButtonVisible()}>
<div class={styles.showMore}>
<Button size="L" onClick={handleLoadMoreClick} value={t('Load more')} />
</div>
</Show>
</div>
</Show>
</div>
)
}

View File

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

View File

@ -1,6 +1,6 @@
import { createMemo, createSignal, For, onMount, Show } from 'solid-js' import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
import Banner from '../Discours/Banner' import Banner from '../Discours/Banner'
import { NavTopics } from '../Nav/Topics' import { Topics } from '../Nav/Topics'
import { Row5 } from '../Feed/Row5' import { Row5 } from '../Feed/Row5'
import { Row3 } from '../Feed/Row3' import { Row3 } from '../Feed/Row3'
import { Row2 } from '../Feed/Row2' import { Row2 } from '../Feed/Row2'
@ -104,7 +104,7 @@ export const HomeView = (props: Props) => {
return ( return (
<Show when={sortedArticles().length > 0}> <Show when={sortedArticles().length > 0}>
<NavTopics /> <Topics />
<Row5 articles={sortedArticles().slice(0, 5)} nodate={true} /> <Row5 articles={sortedArticles().slice(0, 5)} nodate={true} />

View File

@ -1,3 +1,7 @@
.withPadding { .withPadding {
padding-top: 58px; padding-top: 58px;
} }
.zeroBottomPadding {
padding-bottom: 0;
}

View File

@ -18,6 +18,7 @@ type Props = {
hideFooter?: boolean hideFooter?: boolean
class?: string class?: string
withPadding?: boolean withPadding?: boolean
zeroBottomPadding?: boolean
scrollToComments?: (value: boolean) => void scrollToComments?: (value: boolean) => void
} }
@ -44,7 +45,8 @@ export const PageLayout = (props: Props) => {
/> />
<main <main
class={clsx('main-content', { class={clsx('main-content', {
[styles.withPadding]: props.withPadding [styles.withPadding]: props.withPadding,
[styles.zeroBottomPadding]: props.zeroBottomPadding
})} })}
classList={{ 'main-content--no-padding': !isHeaderFixed }} classList={{ 'main-content--no-padding': !isHeaderFixed }}
> >

View File

@ -44,7 +44,7 @@ export const CreatePage = () => {
</div> </div>
</li> </li>
<li> <li>
<div class={styles.link} onClick={() => handleCreate('audio')}> <div class={styles.link} onClick={() => handleCreate('music')}>
<Icon name="create-music" class={styles.icon} /> <Icon name="create-music" class={styles.icon} />
<div>{t('music')}</div> <div>{t('music')}</div>
</div> </div>

View File

@ -0,0 +1,4 @@
import { ROUTES } from '../../stores/router'
import { getServerRoute } from '../../utils/getServerRoute'
export default getServerRoute(ROUTES.expo)

View File

@ -0,0 +1,17 @@
import type { PageContext } from '../../renderer/types'
import { apiClient } from '../../utils/apiClient'
import type { PageProps } from '../types'
import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Expo/Expo'
export const onBeforeRender = async (_pageContext: PageContext) => {
const expoShouts = await apiClient.getShouts({
filters: { excludeLayout: 'article' },
limit: PRERENDERED_ARTICLES_COUNT
})
const pageProps: PageProps = { expoShouts }
return {
pageContext: {
pageProps
}
}
}

View File

@ -0,0 +1,44 @@
import { PageLayout } from '../../components/_shared/PageLayout'
import type { PageProps } from '../types'
import { Topics } from '../../components/Nav/Topics'
import { Expo } from '../../components/Views/Expo'
import { useLocalize } from '../../context/localize'
import { createMemo } from 'solid-js'
import { LayoutType } from '../types'
import { useRouter } from '../../stores/router'
import { Title } from '@solidjs/meta'
export const ExpoPage = (props: PageProps) => {
const { t } = useLocalize()
const { page: getPage } = useRouter()
const getLayout = createMemo<LayoutType>(() => getPage().params['layout'] as LayoutType)
const title = createMemo(() => {
switch (getLayout()) {
case 'music': {
return t('Audio')
}
case 'video': {
return t('Video')
}
case 'image': {
return t('Artworks')
}
case 'literature': {
return t('Literature')
}
default: {
return t('Art')
}
}
})
return (
<PageLayout withPadding={true} zeroBottomPadding={true}>
<Title>{title()}</Title>
<Topics />
<Expo shouts={props.expoShouts} />
</PageLayout>
)
}
export const Page = ExpoPage

View File

@ -0,0 +1,4 @@
import { ROUTES } from '../../stores/router'
import { getServerRoute } from '../../utils/getServerRoute'
export default getServerRoute(ROUTES.expoLayout)

View File

@ -0,0 +1,20 @@
import type { PageContext } from '../../renderer/types'
import { apiClient } from '../../utils/apiClient'
import type { PageProps } from '../types'
import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Expo/Expo'
export const onBeforeRender = async (pageContext: PageContext) => {
const { layout } = pageContext.routeParams
const expoShouts = await apiClient.getShouts({
filters: { layout: layout },
limit: PRERENDERED_ARTICLES_COUNT
})
const pageProps: PageProps = { expoShouts }
return {
pageContext: {
pageProps
}
}
}

View File

@ -1,4 +0,0 @@
import { ROUTES } from '../stores/router'
import { getServerRoute } from '../utils/getServerRoute'
export default getServerRoute(ROUTES.expo)

View File

@ -1,18 +0,0 @@
import type { PageContext } from '../renderer/types'
import { apiClient } from '../utils/apiClient'
import type { PageProps } from './types'
import { PRERENDERED_ARTICLES_COUNT } from './layoutShouts.page'
export const onBeforeRender = async (pageContext: PageContext) => {
const { layout } = pageContext.routeParams
const layoutShouts = await apiClient.getShouts({ filters: { layout }, limit: PRERENDERED_ARTICLES_COUNT })
const pageProps: PageProps = { layoutShouts }
return {
pageContext: {
pageProps
}
}
}

View File

@ -1,187 +0,0 @@
import { PageLayout } from '../components/_shared/PageLayout'
import type { LayoutType, PageProps } from './types'
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
import { loadShouts, resetSortedArticles, useArticlesStore } from '../stores/zine/articles'
import { router, useRouter } from '../stores/router'
import { Loading } from '../components/_shared/Loading'
import { restoreScrollPosition, saveScrollPosition } from '../utils/scroll'
import type { Shout } from '../graphql/types.gen'
import { splitToPages } from '../utils/splitToPages'
import { clsx } from 'clsx'
import { Row3 } from '../components/Feed/Row3'
import { Row2 } from '../components/Feed/Row2'
import { Beside } from '../components/Feed/Beside'
import { Slider } from '../components/_shared/Slider'
import { Row1 } from '../components/Feed/Row1'
import styles from '../styles/Topic.module.scss'
import { ArticleCard } from '../components/Feed/ArticleCard'
import { useLocalize } from '../context/localize'
import { getPagePath } from '@nanostores/router'
import { Title } from '@solidjs/meta'
export const PRERENDERED_ARTICLES_COUNT = 27
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
export const LayoutShoutsPage = (props: PageProps) => {
const { t } = useLocalize()
const { page: getPage } = useRouter()
const getLayout = createMemo<LayoutType>(() => getPage().params['layout'] as LayoutType)
const [isLoaded, setIsLoaded] = createSignal(
Boolean(props.layoutShouts) &&
props.layoutShouts.length > 0 &&
props.layoutShouts[0].layout === getLayout()
)
const { sortedArticles } = useArticlesStore({
shouts: isLoaded() ? props.layoutShouts : []
})
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const loadMore = async (count) => {
saveScrollPosition()
const { hasMore } = await loadShouts({
filters: { layout: getLayout() },
limit: count,
offset: sortedArticles().length
})
setIsLoadMoreButtonVisible(hasMore)
restoreScrollPosition()
}
const title = createMemo(() => {
const l = getLayout()
if (l === 'audio') return t('Audio')
if (l === 'video') return t('Video')
if (l === 'image') return t('Artworks')
return t('Literature')
})
const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
)
onMount(() => {
if (isLoaded()) {
return
}
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
setIsLoaded(true)
})
onMount(() => {
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
loadMore(LOAD_MORE_PAGE_SIZE)
}
})
createEffect((prevLayout) => {
if (prevLayout !== getLayout()) {
resetSortedArticles()
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
}
return getLayout()
}, getLayout())
onCleanup(() => {
resetSortedArticles()
})
const handleLoadMoreClick = () => {
loadMore(LOAD_MORE_PAGE_SIZE)
}
return (
<PageLayout>
<Title>{title()}</Title>
<Show when={isLoaded()} fallback={<Loading />}>
<div class={styles.topicPage}>
<Show when={getLayout()}>
<div class="wide-container">
<h1>{title()}</h1>
</div>
<div class="wide-container">
<div class={clsx(styles.groupControls, 'row group__controls')}>
<div class="col-md-16">
<ul class="view-switcher">
<li classList={{ 'view-switcher__item--selected': getLayout() === 'audio' }}>
<a href={getPagePath(router, 'expo', { layout: 'audio' })}>{t('Audio')}</a>
</li>
<li classList={{ 'view-switcher__item--selected': getLayout() === 'video' }}>
<a href={getPagePath(router, 'expo', { layout: 'video' })}>{t('Video')}</a>
</li>
<li classList={{ 'view-switcher__item--selected': getLayout() === 'image' }}>
<a href={getPagePath(router, 'expo', { layout: 'image' })}>{t('Artworks')}</a>
</li>
<li classList={{ 'view-switcher__item--selected': getLayout() === 'literature' }}>
<a href={getPagePath(router, 'expo', { layout: 'literature' })}>{t('Literature')}</a>
</li>
</ul>
</div>
</div>
</div>
<Show when={sortedArticles().length > 0} fallback={<Loading />}>
<Row1 article={sortedArticles()[0]} />
<Row2 articles={sortedArticles().slice(1, 3)} />
<Slider title={title()}>
<For each={sortedArticles().slice(5, 11)}>
{(article) => (
<ArticleCard
article={article}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<Beside
beside={sortedArticles()[12]}
title={t('Top viewed')}
values={sortedArticles().slice(0, 5)}
wrapper={'top-article'}
/>
<Show when={sortedArticles().length > 5}>
<Row3 articles={sortedArticles().slice(13, 16)} />
<Row2 articles={sortedArticles().slice(16, 18)} />
<Row3 articles={sortedArticles().slice(18, 21)} />
<Row3 articles={sortedArticles().slice(21, 24)} />
<Row3 articles={sortedArticles().slice(24, 27)} />
</Show>
<For each={pages()}>
{(page) => (
<>
<Row3 articles={page.slice(0, 3)} />
<Row3 articles={page.slice(3, 6)} />
<Row3 articles={page.slice(6, 9)} />
</>
)}
</For>
<Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container">
<button class="button" onClick={handleLoadMoreClick}>
{t('Load more')}
</button>
</p>
</Show>
</Show>
</Show>
</div>
</Show>
</PageLayout>
)
}
export const Page = LayoutShoutsPage

View File

@ -4,7 +4,7 @@ import type { Author, Chat, Shout, Topic } from '../graphql/types.gen'
// all the things (she said) that could be passed from the server // all the things (she said) that could be passed from the server
export type PageProps = { export type PageProps = {
article?: Shout article?: Shout
layoutShouts?: Shout[] expoShouts?: Shout[]
authorShouts?: Shout[] authorShouts?: Shout[]
topicShouts?: Shout[] topicShouts?: Shout[]
homeShouts?: Shout[] homeShouts?: Shout[]
@ -24,7 +24,7 @@ export type RootSearchParams = {
lang: string lang: string
} }
export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature' export type LayoutType = 'article' | 'music' | 'video' | 'image' | 'literature'
export type FileTypeToUpload = 'image' | 'video' | 'doc' | 'audio' export type FileTypeToUpload = 'image' | 'video' | 'doc' | 'audio'

View File

@ -37,7 +37,8 @@ export const ROUTES = {
projects: '/about/projects', projects: '/about/projects',
termsOfUse: '/about/terms-of-use', termsOfUse: '/about/terms-of-use',
thanks: '/about/thanks', thanks: '/about/thanks',
expo: '/expo/:layout', expo: '/expo',
expoLayout: '/expo/:layout',
profileSettings: '/profile/settings', profileSettings: '/profile/settings',
profileSecurity: '/profile/security', profileSecurity: '/profile/security',
profileSubscriptions: '/profile/subscriptions', profileSubscriptions: '/profile/subscriptions',

View File

@ -623,7 +623,7 @@ figure {
a, a,
.linkReplacement, .linkReplacement,
button { button {
border-bottom: 2px solid #fff; border-bottom: 2px solid transparent;
color: var(--link-color); color: var(--link-color);
cursor: pointer; cursor: pointer;
font-weight: inherit; font-weight: inherit;

View File

@ -325,7 +325,6 @@ export const apiClient = {
getShouts: async (options: LoadShoutsOptions) => { getShouts: async (options: LoadShoutsOptions) => {
const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise() const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise()
if (resp.error) { if (resp.error) {
console.error(resp) console.error(resp)
} }